From a0be094cbde68221bfd80908c81901243c072190 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Mon, 27 Oct 2025 20:53:51 +0100 Subject: [PATCH] table --- frontend/src/documents/DocumentsTable.jsx | 122 ++++++++++++++++++++-- frontend/src/index.jsx | 2 + frontend/src/styles.css | 57 ++++++++++ 3 files changed, 172 insertions(+), 9 deletions(-) diff --git a/frontend/src/documents/DocumentsTable.jsx b/frontend/src/documents/DocumentsTable.jsx index 780e22a..74b95de 100644 --- a/frontend/src/documents/DocumentsTable.jsx +++ b/frontend/src/documents/DocumentsTable.jsx @@ -13,6 +13,40 @@ const getPageCount = (doc) => ? doc.current_version.metadata.page_count : null; +const resolveCorrespondents = (doc) => { + if (!doc || !Array.isArray(doc.correspondents)) { + return []; + } + + const seen = new Set(); + const results = []; + + doc.correspondents.forEach((entry, index) => { + if (!entry) return; + + const id = entry.id ?? entry.correspondent_id ?? null; + const name = (entry.name || entry.label || entry.slug || '').trim(); + if (!name) return; + + if (id && seen.has(id)) { + return; + } + + if (id) { + seen.add(id); + } + + results.push({ + id, + name, + role: entry.role || null, + key: id ?? `${name}-${index}`, + }); + }); + + return results; +}; + // Detects when an element becomes visible within a scroll container. const useLazyVisibility = (rootRef, resetKey) => { const targetRef = useRef(null); @@ -165,6 +199,7 @@ const DocumentsTable = ({ onFolderRename, onDocumentRename, tagLookupById, + activeCorrespondentIds = [], onDocumentListFocus, onDocumentListKeyDown, onFocusedRowChange, @@ -172,6 +207,7 @@ const DocumentsTable = ({ getDocumentAsset = () => null, getDownloadHref, onTagClick, + onCorrespondentClick, isSearchLoading = false, onDocumentTagDrop, viewMode = 'list', @@ -194,6 +230,10 @@ const DocumentsTable = ({ () => new Set(draggingDocumentIds || []), [draggingDocumentIds], ); + const activeCorrespondentIdSet = useMemo( + () => new Set(activeCorrespondentIds || []), + [activeCorrespondentIds], + ); const scrollRef = useRef(null); const [, forceVisibilityTick] = useState(0); const lastScrollNodeRef = useRef(null); @@ -333,6 +373,51 @@ const DocumentsTable = ({ [isTagDragEvent, onDocumentTagDrop], ); + const renderCorrespondentLinks = useCallback( + (correspondents) => + correspondents.map((correspondent, index) => { + const isActive = correspondent.id != null && activeCorrespondentIdSet.has(correspondent.id); + const hasClickHandler = Boolean(onCorrespondentClick) && correspondent.id != null; + const classNames = ['doc-correspondent-link']; + if (isActive) classNames.push('is-active'); + if (!hasClickHandler) classNames.push('is-static'); + const isLast = index === correspondents.length - 1; + const label = isLast + ? `${correspondent.name}:${String.fromCharCode(160)}` + : correspondent.name; + return ( + + + {!isLast ? ( + , + ) : null} + + ); + }), + [activeCorrespondentIdSet, onCorrespondentClick], + ); + const showDefaultEmptyState = !showingSearchResults && !subfolders.length && rows.length === 0; const showListSearchEmptyState = showingSearchResults && rows.length === 0 && !isGridView && !isSearchLoading; @@ -498,6 +583,7 @@ const DocumentsTable = ({ const tagList = Array.isArray(doc.tags) ? doc.tags : []; const visibleTags = tagList.slice(0, 3); const remainingTagCount = tagList.length > 3 ? tagList.length - 3 : 0; + const correspondents = resolveCorrespondents(doc); const cardClasses = ['document-card', 'document']; if (isSelected) cardClasses.push('selected'); if (isDraggingDoc) cardClasses.push('is-dragging'); @@ -533,7 +619,12 @@ const DocumentsTable = ({ className="document-card__title" title={doc.title || doc.original_name} > - {doc.title || doc.original_name} + {correspondents.length > 0 ? ( + + {renderCorrespondentLinks(correspondents)} + + ) : null} + {doc.title || doc.original_name} {visibleTags.length > 0 && (
@@ -606,8 +697,7 @@ const DocumentsTable = ({ Preview Name - Type - Updated + Issued Actions @@ -671,7 +761,6 @@ const DocumentsTable = ({ {folder.name}
- Folder —
@@ -721,6 +810,7 @@ const DocumentsTable = ({ if (isSelected) rowClasses.push('selected'); if (isDraggingDoc) rowClasses.push('is-dragging'); const downloadHref = getDownloadHref?.(doc) || null; + const correspondents = resolveCorrespondents(doc); return (
- {doc.title || doc.original_name} + + {correspondents.length > 0 ? ( + + {renderCorrespondentLinks(correspondents)} + + ) : null} + {doc.title || doc.original_name} +
{(doc.tags || []).length > 0 && (
@@ -810,11 +907,18 @@ const DocumentsTable = ({ )}
- {doc.content_type || 'Document'} - {doc.updated_at - ? new Date(doc.updated_at).toLocaleString() - : '—'} + {(() => { + const issuedAt = doc.issued_at || doc.updated_at || null; + if (!issuedAt) { + return '—'; + } + const timestamp = Date.parse(issuedAt); + if (Number.isNaN(timestamp)) { + return '—'; + } + return new Date(timestamp).toLocaleDateString(); + })()}
diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx index 2de5e1b..64bb048 100644 --- a/frontend/src/index.jsx +++ b/frontend/src/index.jsx @@ -4856,6 +4856,7 @@ const AppLayout = () => { onDocumentDragEnd: handleDocumentDragEnd, isSearchLoading: searchLoading, tagLookupById, + activeCorrespondentIds: activeCorrespondentFilters, onDocumentListFocus: handleDocumentListFocus, onDocumentListKeyDown: handleDocumentListKeyDown, onFocusedRowChange: setFocusedRowKey, @@ -4866,6 +4867,7 @@ const AppLayout = () => { ? resolveApiPath(doc.current_version.download_path) : null, onTagClick: toggleTagFilter, + onCorrespondentClick: toggleCorrespondentFilter, onDocumentTagDrop: handleDocumentTagDrop, viewMode: documentsViewMode, onViewModeChange: handleDocumentsViewModeChange, diff --git a/frontend/src/styles.css b/frontend/src/styles.css index b205cab..72c41f5 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1631,6 +1631,63 @@ button.danger:hover:not([disabled]) { gap: 0.25rem; } +.doc-correspondents { + display: inline; + color: inherit; +} + +.doc-correspondent-link { + background: none; + background-color: transparent; + border: none; + padding: 0; + margin: 0; + color: var(--accent, #2563eb); + text-decoration: none; + font: inherit; + cursor: pointer; + box-shadow: none; + appearance: none; +} + +.doc-correspondent-link:not(.is-static):hover, +.doc-correspondent-link:not(.is-static):focus-visible { + color: var(--accent-strong, var(--accent)); + text-decoration: underline; + text-decoration-thickness: 1.5px; + background-color: transparent; + box-shadow: none; +} + +.doc-correspondent-link:not(.is-static):focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; +} + +.documents-panel tbody tr.document.selected .doc-correspondents, +.documents-panel tbody tr.document.selected .doc-correspondent-link, +.document-card.selected .doc-correspondent-link { + color: inherit; +} + +.document-card.selected .doc-correspondents { + color: inherit; +} + +.doc-correspondent-link.is-active { + font-weight: 600; +} + +.doc-correspondent-link.is-static { + color: inherit; + cursor: default; + text-decoration: none; +} + +.doc-correspondent-link__separator { + color: inherit; +} + .documents-panel tbody tr.document.dragging { opacity: 0.4; }