table
This commit is contained in:
@@ -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 (
|
||||
<React.Fragment key={correspondent.key ?? correspondent.id ?? `${correspondent.name}-${index}`}>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames.join(' ')}
|
||||
aria-disabled={hasClickHandler ? undefined : true}
|
||||
onClick={(event) => {
|
||||
if (!hasClickHandler) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
onCorrespondentClick(correspondent.id, correspondent);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (!hasClickHandler) {
|
||||
return;
|
||||
}
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
{!isLast ? (
|
||||
<span className="doc-correspondent-link__separator">, </span>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
}),
|
||||
[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 ? (
|
||||
<span className="doc-correspondents">
|
||||
{renderCorrespondentLinks(correspondents)}
|
||||
</span>
|
||||
) : null}
|
||||
<span>{doc.title || doc.original_name}</span>
|
||||
</div>
|
||||
{visibleTags.length > 0 && (
|
||||
<div className="document-card__tags">
|
||||
@@ -606,8 +697,7 @@ const DocumentsTable = ({
|
||||
<tr>
|
||||
<th className="thumb-column">Preview</th>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Updated</th>
|
||||
<th>Issued</th>
|
||||
<th className="actions-column">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -671,7 +761,6 @@ const DocumentsTable = ({
|
||||
<span>{folder.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>Folder</td>
|
||||
<td>—</td>
|
||||
<td className="actions">
|
||||
<div className="action-buttons">
|
||||
@@ -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 (
|
||||
<tr
|
||||
@@ -749,7 +839,14 @@ const DocumentsTable = ({
|
||||
<td className="doc-list__name">
|
||||
<div className="doc-name">
|
||||
<div className="doc-list__name-content">
|
||||
<span className="doc-name__title">{doc.title || doc.original_name}</span>
|
||||
<span className="doc-name__title">
|
||||
{correspondents.length > 0 ? (
|
||||
<span className="doc-correspondents">
|
||||
{renderCorrespondentLinks(correspondents)}
|
||||
</span>
|
||||
) : null}
|
||||
<span>{doc.title || doc.original_name}</span>
|
||||
</span>
|
||||
</div>
|
||||
{(doc.tags || []).length > 0 && (
|
||||
<div className="doc-name__tags">
|
||||
@@ -810,11 +907,18 @@ const DocumentsTable = ({
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>{doc.content_type || 'Document'}</td>
|
||||
<td>
|
||||
{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();
|
||||
})()}
|
||||
</td>
|
||||
<td className="actions">
|
||||
<div className="action-buttons">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user