This commit is contained in:
2025-11-02 00:35:32 +01:00
parent 49998fa23d
commit 09430c6f40
6 changed files with 867 additions and 782 deletions
+217
View File
@@ -0,0 +1,217 @@
import React from 'react';
import { FolderIcon } from '../ui/icons';
import DocumentThumbnailImage from './DocumentThumbnailImage';
import CorrespondentLinks from './CorrespondentLinks';
import { getTagColorStyle } from '../utils/colors';
import { resolveCorrespondents } from './correspondents';
const DocumentsGrid = ({
entries,
selectedDocumentIdsSet,
selectedFolderIdsSet,
draggingDocumentIdsSet,
draggedFolderId,
onFolderClick,
onFolderSelect,
onFolderDragOver,
onFolderDragLeave,
onFolderDrop,
onFolderDragStart,
onFolderDragEnd,
onDocumentClick,
onDocumentOpen,
onDocumentDragStart,
onDocumentDragEnd,
onDocumentTagDragOver,
onDocumentTagDragLeave,
onDocumentTagDrop,
ensureAssetUrl,
getDocumentAsset,
gridIconSize,
tagLookupById,
onTagClick,
scrollRef,
onCorrespondentClick,
activeCorrespondentIdSet,
onClearSelection,
}) => (
<div
className="documents-grid"
role="list"
style={{ '--documents-grid-icon-size': `${gridIconSize}px` }}
onClick={(event) => {
if (event.target === event.currentTarget) {
onClearSelection?.();
}
}}
>
{entries.map((entry) => {
if (entry.type === 'folder') {
const folder = entry.folder;
if (!folder) {
return null;
}
const canDragFolder = folder.id !== 'root';
const isDraggingFolder = draggedFolderId === folder.id;
const isSelectedFolder = selectedFolderIdsSet?.has(folder.id);
const classes = ['document-card', 'folder-card'];
if (isDraggingFolder) classes.push('is-dragging');
if (isSelectedFolder) classes.push('selected');
return (
<div
key={entry.key}
className={classes.join(' ')}
role="listitem"
id={`folder-card-${folder.id}`}
draggable={canDragFolder}
onClick={(event) => onFolderClick?.(folder, event)}
onDoubleClick={(event) => {
event.preventDefault();
onFolderSelect?.(folder.id);
}}
onDragOver={(event) => onFolderDragOver?.(event, folder.id)}
onDragLeave={onFolderDragLeave}
onDrop={(event) => onFolderDrop?.(event, folder.id)}
onDragStart={(event) => {
if (canDragFolder) {
onFolderDragStart?.(event, folder.id);
}
}}
onDragEnd={(event) => {
if (canDragFolder) {
onFolderDragEnd?.(event);
}
}}
>
<div className="folder-card__icon">
<FolderIcon className="folder-card__icon-svg" size={gridIconSize} />
</div>
<div className="folder-card__meta">
<div className="folder-card__name" title={folder.name}>
{folder.name}
</div>
</div>
</div>
);
}
const doc = entry.document;
if (!doc) {
return null;
}
const isSelected = selectedDocumentIdsSet?.has(doc.id);
const isDraggingDoc = draggingDocumentIdsSet?.has(doc.id);
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');
const titleText = doc.title || doc.original_name;
return (
<div
key={entry.key}
className={cardClasses.join(' ')}
role="listitem"
id={`document-card-${doc.id}`}
data-doc-id={doc.id}
onClick={(event) => onDocumentClick?.(doc, event)}
onDoubleClick={() => onDocumentOpen?.(doc.id)}
draggable
onDragStart={(event) => onDocumentDragStart?.(event, doc)}
onDragEnd={(event) => onDocumentDragEnd?.(event)}
onDragOver={(event) => onDocumentTagDragOver?.(event)}
onDragOverCapture={(event) => onDocumentTagDragOver?.(event)}
onDragLeave={onDocumentTagDragLeave}
onDragLeaveCapture={onDocumentTagDragLeave}
onDrop={(event) => onDocumentTagDrop?.(event, doc.id)}
onDropCapture={(event) => onDocumentTagDrop?.(event, doc.id)}
>
<DocumentThumbnailImage
document={doc}
ensureAssetUrl={ensureAssetUrl}
getDocumentAsset={getDocumentAsset}
alt={`Thumbnail for ${titleText}`}
maxSize={gridIconSize}
scrollRootRef={scrollRef}
/>
<div className="document-card__meta">
<div className="document-card__title" title={titleText}>
{correspondents.length > 0 ? (
<span className="doc-correspondents">
<CorrespondentLinks
correspondents={correspondents}
activeCorrespondentIdSet={activeCorrespondentIdSet}
onCorrespondentClick={onCorrespondentClick}
/>
</span>
) : null}
<span className="doc-name__primary">{titleText}</span>
</div>
{visibleTags.length > 0 && (
<div className="document-card__tags">
{visibleTags.map((tag) => {
const colorSource = tag?.color || tagLookupById?.get(tag.id)?.color;
const style = getTagColorStyle(colorSource);
return (
<span
key={tag.id}
className="badge tag-chip"
style={style || undefined}
title={tag.label}
role="button"
onClick={(event) => {
event.stopPropagation();
onTagClick?.(tag.id);
}}
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 (error) {
console.warn('[documents] Failed to configure tag drag payload', error);
}
}}
onDragEnd={(event) => {
event.stopPropagation();
}}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.stopPropagation();
onTagClick?.(tag.id);
}
}}
>
{tag.label}
</span>
);
})}
{remainingTagCount > 0 && (
<span className="badge tag-chip tag-chip--more">+{remainingTagCount}</span>
)}
</div>
)}
</div>
</div>
);
})}
</div>
);
export default DocumentsGrid;