grid view
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
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';
|
||||
import { DownloadIcon, FolderIcon, EditIcon, ViewListIcon, ViewGridIcon } from '../ui/icons';
|
||||
|
||||
const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
|
||||
|
||||
@@ -74,6 +74,8 @@ const DocumentsTable = ({
|
||||
onTagClick,
|
||||
isSearchLoading = false,
|
||||
onDocumentTagDrop,
|
||||
viewMode = 'list',
|
||||
onViewModeChange,
|
||||
}) => {
|
||||
const showingSearchResults = searchResults !== null;
|
||||
const rows = showingSearchResults ? searchResults : documents;
|
||||
@@ -91,6 +93,19 @@ const DocumentsTable = ({
|
||||
[draggingDocumentIds],
|
||||
);
|
||||
const scrollRef = useRef(null);
|
||||
const isGridView = viewMode === 'grid';
|
||||
const handleSetViewMode = useCallback(
|
||||
(nextMode) => {
|
||||
if (!onViewModeChange) {
|
||||
return;
|
||||
}
|
||||
onViewModeChange(nextMode);
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = 0;
|
||||
}
|
||||
},
|
||||
[onViewModeChange],
|
||||
);
|
||||
const isTagDragEvent = useCallback((event) => {
|
||||
const types = Array.from(event.dataTransfer?.types || []);
|
||||
return TAG_MIME_TYPES.some((type) => types.includes(type));
|
||||
@@ -199,7 +214,9 @@ const DocumentsTable = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="documents-panel column">
|
||||
<section
|
||||
className={`documents-panel column documents-panel--view-${isGridView ? 'grid' : 'list'}`}
|
||||
>
|
||||
<div className="column-header">
|
||||
<div className="column-header__titles">
|
||||
<nav className="breadcrumb" aria-label="Folder breadcrumbs">
|
||||
@@ -230,6 +247,26 @@ const DocumentsTable = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
<div className="view-toggle" role="group" aria-label="Change view">
|
||||
<button
|
||||
type="button"
|
||||
className={`view-toggle__button${isGridView ? '' : ' active'}`}
|
||||
onClick={() => handleSetViewMode('list')}
|
||||
aria-pressed={!isGridView}
|
||||
title="List view"
|
||||
>
|
||||
<ViewListIcon className="view-toggle__icon" size={18} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`view-toggle__button${isGridView ? ' active' : ''}`}
|
||||
onClick={() => handleSetViewMode('grid')}
|
||||
aria-pressed={isGridView}
|
||||
title="Icons view"
|
||||
>
|
||||
<ViewGridIcon className="view-toggle__icon" size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRequestCreateFolder}
|
||||
@@ -263,12 +300,176 @@ const DocumentsTable = ({
|
||||
onDocumentListKeyDown(event);
|
||||
}
|
||||
}}
|
||||
aria-activedescendant={activeDescendantId}
|
||||
aria-activedescendant={isGridView ? undefined : activeDescendantId}
|
||||
>
|
||||
{!showingSearchResults && !subfolders.length && rows.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
Drop files anywhere or onto a folder to upload documents.
|
||||
</div>
|
||||
) : isGridView ? (
|
||||
<div className="documents-grid" role="list">
|
||||
{!showingSearchResults &&
|
||||
subfolders.map((folder) => {
|
||||
const canDragFolder = folder.id !== 'root';
|
||||
const isDraggingFolder = draggedFolderId === folder.id;
|
||||
const isSelectedFolder = selectedFolderSet.has(folder.id);
|
||||
const classes = ['document-card', 'folder-card'];
|
||||
if (isDraggingFolder) classes.push('is-dragging');
|
||||
if (isSelectedFolder) classes.push('selected');
|
||||
return (
|
||||
<div
|
||||
key={folder.id}
|
||||
className={classes.join(' ')}
|
||||
role="listitem"
|
||||
id={`folder-card-${folder.id}`}
|
||||
draggable={canDragFolder}
|
||||
onClick={(event) => {
|
||||
onFolderRowClick?.(folder.id, event);
|
||||
const shouldNavigate =
|
||||
!event.defaultPrevented &&
|
||||
!event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.shiftKey;
|
||||
if (shouldNavigate) {
|
||||
onFolderSelect(folder.id);
|
||||
}
|
||||
}}
|
||||
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="icon-inline" size={32} />
|
||||
</div>
|
||||
<div className="folder-card__meta">
|
||||
<div className="folder-card__name" title={folder.name}>
|
||||
{folder.name}
|
||||
</div>
|
||||
<div className="folder-card__badge">Folder</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{rows.map((doc) => {
|
||||
const isSelected = selectedSet.has(doc.id);
|
||||
const isDraggingDoc = draggingSet.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 cardClasses = ['document-card', 'document'];
|
||||
if (isSelected) cardClasses.push('selected');
|
||||
if (isDraggingDoc) cardClasses.push('is-dragging');
|
||||
return (
|
||||
<div
|
||||
key={doc.id}
|
||||
className={cardClasses.join(' ')}
|
||||
role="listitem"
|
||||
id={`document-card-${doc.id}`}
|
||||
onClick={(event) => onDocumentRowClick(doc.id, event)}
|
||||
onDoubleClick={() => onDocumentOpen(doc.id)}
|
||||
draggable
|
||||
onDragStart={(event) => onDocumentDragStart(event, doc)}
|
||||
onDragEnd={onDocumentDragEnd}
|
||||
onDragOver={(event) => handleDocumentTagDragOver(event)}
|
||||
onDragOverCapture={(event) => handleDocumentTagDragOver(event)}
|
||||
onDragLeave={handleDocumentTagDragLeave}
|
||||
onDragLeaveCapture={handleDocumentTagDragLeave}
|
||||
onDrop={(event) => handleDocumentTagDrop(event, doc.id)}
|
||||
onDropCapture={(event) => handleDocumentTagDrop(event, doc.id)}
|
||||
>
|
||||
<DocumentThumbnailImage
|
||||
document={doc}
|
||||
ensureAssetUrl={ensureAssetUrl}
|
||||
getDocumentAsset={getDocumentAsset}
|
||||
alt={`Thumbnail for ${doc.title || doc.original_name}`}
|
||||
/>
|
||||
<div className="document-card__meta">
|
||||
<div
|
||||
className="document-card__title"
|
||||
title={doc.title || doc.original_name}
|
||||
>
|
||||
{doc.title || doc.original_name}
|
||||
</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}
|
||||
onClick={(event) => {
|
||||
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}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{remainingTagCount > 0 && (
|
||||
<span className="badge tag-chip tag-chip--more">+{remainingTagCount}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<table aria-multiselectable="true">
|
||||
<thead>
|
||||
@@ -539,7 +740,7 @@ const DocumentsTable = ({
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
{rows.length === 0 && showingSearchResults && !isSearchLoading && (
|
||||
{rows.length === 0 && showingSearchResults && !isGridView && !isSearchLoading && (
|
||||
<div className="empty-state">No documents match the current filters.</div>
|
||||
)}
|
||||
{showingSearchResults && rows.length > 0 && (
|
||||
@@ -548,6 +749,11 @@ const DocumentsTable = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isGridView && showingSearchResults && rows.length === 0 && !isSearchLoading && (
|
||||
<div className="empty-state empty-state--global">
|
||||
No documents match the current filters.
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user