refactor: Decompose document and folder views into modular components and hooks, replacing monolithic list/grid implementations.
This commit is contained in:
@@ -1,441 +0,0 @@
|
|||||||
import React, { useMemo } from 'react';
|
|
||||||
import { FolderIcon, CheckIcon, CloseIcon } from '../ui/icons';
|
|
||||||
import DocumentThumbnailImage from './DocumentThumbnailImage';
|
|
||||||
import CorrespondentLinks from './CorrespondentLinks';
|
|
||||||
import { getTagColorStyle } from '../utils/colors';
|
|
||||||
import { resolveCorrespondents } from './correspondents';
|
|
||||||
import { writeTagTransferData, parseTagTransferPayload } from './tagTransfer';
|
|
||||||
import useInlineRename from './useInlineRename';
|
|
||||||
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
Folder as FolderLike,
|
|
||||||
Document,
|
|
||||||
} from '../types/documents';
|
|
||||||
import type { DocumentsViewProps } from './panel/DocumentsPanel';
|
|
||||||
|
|
||||||
interface DocumentsGridProps extends DocumentsViewProps {
|
|
||||||
gridIconSize?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DocumentsGrid: React.FC<DocumentsGridProps> = ({
|
|
||||||
entries,
|
|
||||||
draggingDocumentIdsSet,
|
|
||||||
draggedFolderId,
|
|
||||||
onFolderClick,
|
|
||||||
onFolderSelect,
|
|
||||||
onFolderDragOver,
|
|
||||||
onFolderDragLeave,
|
|
||||||
onFolderDrop,
|
|
||||||
onFolderDragStart,
|
|
||||||
onFolderDragEnd,
|
|
||||||
onDocumentClick,
|
|
||||||
onDocumentActivate,
|
|
||||||
onDocumentDragStart,
|
|
||||||
onDocumentDragEnd,
|
|
||||||
onDocumentTagDragOver,
|
|
||||||
onDocumentTagDragLeave,
|
|
||||||
onDocumentTagDrop,
|
|
||||||
ensureAssetUrl,
|
|
||||||
getDocumentAsset,
|
|
||||||
gridIconSize,
|
|
||||||
tagLookupById,
|
|
||||||
onTagClick,
|
|
||||||
scrollRef,
|
|
||||||
onCorrespondentClick,
|
|
||||||
activeCorrespondentIdSet,
|
|
||||||
onDocumentRename,
|
|
||||||
onFolderRename,
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
selectedDocumentIds,
|
|
||||||
selectedFolderIds,
|
|
||||||
clearSelection,
|
|
||||||
} = useWorkspaceSelectionContext();
|
|
||||||
const selectedDocumentIdsSet = useMemo(() => new Set(selectedDocumentIds), [selectedDocumentIds]);
|
|
||||||
const selectedFolderIdsSet = useMemo(() => new Set(selectedFolderIds || []), [selectedFolderIds]);
|
|
||||||
const {
|
|
||||||
editingId: editingDocumentId,
|
|
||||||
draftValue: documentDraft,
|
|
||||||
setDraftValue: setDocumentDraft,
|
|
||||||
beginEditing: beginDocumentEditing,
|
|
||||||
cancelEditing: cancelDocumentEditing,
|
|
||||||
submitEditing: submitDocumentEditing,
|
|
||||||
savingId: savingDocumentId,
|
|
||||||
attachInputRef: attachDocumentInputRef,
|
|
||||||
} = useInlineRename<Document>(onDocumentRename, {
|
|
||||||
getCurrentValue: (doc: Document) => doc?.title ?? '',
|
|
||||||
getEntityId: (doc: Document) => doc?.id ?? null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
editingId: editingFolderId,
|
|
||||||
draftValue: folderDraft,
|
|
||||||
setDraftValue: setFolderDraft,
|
|
||||||
beginEditing: beginFolderEditing,
|
|
||||||
cancelEditing: cancelFolderEditing,
|
|
||||||
submitEditing: submitFolderEditing,
|
|
||||||
savingId: savingFolderId,
|
|
||||||
attachInputRef: attachFolderInputRef,
|
|
||||||
} = useInlineRename<FolderLike>(onFolderRename, {
|
|
||||||
getCurrentValue: (folder: FolderLike) => folder?.name ?? '',
|
|
||||||
getEntityId: (folder: FolderLike) => folder?.id ?? null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const documentSelectionCount = selectedDocumentIdsSet?.size ?? 0;
|
|
||||||
const folderSelectionCount = selectedFolderIdsSet?.size ?? 0;
|
|
||||||
const totalSelectionCount = documentSelectionCount + folderSelectionCount;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="documents-grid"
|
|
||||||
role="list"
|
|
||||||
style={{ '--documents-grid-icon-size': `${gridIconSize}px` }}
|
|
||||||
onClick={(event) => {
|
|
||||||
if (event.target === event.currentTarget) {
|
|
||||||
clearSelection();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{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');
|
|
||||||
const canRenameFolder = Boolean(onFolderRename) && folder.id !== 'root';
|
|
||||||
const isFolderEditing = editingFolderId === folder.id;
|
|
||||||
const folderDraftValue = isFolderEditing ? folderDraft : folder.name;
|
|
||||||
const trimmedFolderDraft = isFolderEditing ? folderDraft.trim() : '';
|
|
||||||
const isFolderSaving = savingFolderId === folder.id;
|
|
||||||
const canSubmitFolder =
|
|
||||||
isFolderEditing && trimmedFolderDraft.length > 0 && trimmedFolderDraft !== folder.name;
|
|
||||||
const allowInlineFolderEdit = canRenameFolder && isSelectedFolder && totalSelectionCount === 1;
|
|
||||||
|
|
||||||
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">
|
|
||||||
{isFolderEditing ? (
|
|
||||||
<div className="folder-card__edit doc-title-edit">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
ref={attachFolderInputRef}
|
|
||||||
value={folderDraftValue}
|
|
||||||
onChange={(event) => setFolderDraft(event.target.value)}
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
submitFolderEditing(folder);
|
|
||||||
} else if (event.key === 'Escape') {
|
|
||||||
event.preventDefault();
|
|
||||||
cancelFolderEditing(event);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={(event) => {
|
|
||||||
const nextFocus = event.relatedTarget;
|
|
||||||
if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) {
|
|
||||||
cancelFolderEditing();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="icon-button"
|
|
||||||
aria-label="Save name"
|
|
||||||
title="Save name"
|
|
||||||
disabled={!canSubmitFolder || isFolderSaving}
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
submitFolderEditing(folder);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CheckIcon />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="icon-button"
|
|
||||||
aria-label="Cancel"
|
|
||||||
title="Cancel"
|
|
||||||
onClick={(event) => cancelFolderEditing(event)}
|
|
||||||
>
|
|
||||||
<CloseIcon />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="folder-card__label-row">
|
|
||||||
<span
|
|
||||||
className="folder-card__name"
|
|
||||||
title={folder.name}
|
|
||||||
role={allowInlineFolderEdit ? 'button' : undefined}
|
|
||||||
tabIndex={allowInlineFolderEdit ? 0 : undefined}
|
|
||||||
onClick={(event) => {
|
|
||||||
if (!allowInlineFolderEdit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
beginFolderEditing(folder);
|
|
||||||
}}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (!allowInlineFolderEdit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
beginFolderEditing(folder);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{folder.name}
|
|
||||||
</span>
|
|
||||||
</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 isEditingDoc = editingDocumentId === doc.id;
|
|
||||||
const documentDraftValue = isEditingDoc ? documentDraft : doc.title;
|
|
||||||
const trimmedDocumentDraft = isEditingDoc ? documentDraft.trim() : '';
|
|
||||||
const isDocumentSaving = savingDocumentId === doc.id;
|
|
||||||
const canSubmitDocument =
|
|
||||||
isEditingDoc && trimmedDocumentDraft.length > 0 && trimmedDocumentDraft !== doc.title;
|
|
||||||
const allowInlineDocumentEdit = onDocumentRename && isSelected && totalSelectionCount === 1;
|
|
||||||
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={(event) => onDocumentActivate?.(doc, event)}
|
|
||||||
draggable
|
|
||||||
onDragStart={(event) => onDocumentDragStart?.(event, doc)}
|
|
||||||
onDragEnd={(event) => onDocumentDragEnd?.(event)}
|
|
||||||
onDragOver={(event) => onDocumentTagDragOver?.(event)}
|
|
||||||
onDragOverCapture={(event) => onDocumentTagDragOver?.(event)}
|
|
||||||
onDragLeave={onDocumentTagDragLeave}
|
|
||||||
onDragLeaveCapture={onDocumentTagDragLeave}
|
|
||||||
onDrop={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
const payload = parseTagTransferPayload(event);
|
|
||||||
if (payload && onDocumentTagDrop) {
|
|
||||||
onDocumentTagDrop(doc.id, payload);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
|
|
||||||
>
|
|
||||||
<DocumentThumbnailImage
|
|
||||||
document={doc}
|
|
||||||
ensureAssetUrl={ensureAssetUrl}
|
|
||||||
getDocumentAsset={getDocumentAsset}
|
|
||||||
alt={`Thumbnail for ${doc.title}`}
|
|
||||||
maxSize={gridIconSize}
|
|
||||||
scrollRootRef={scrollRef}
|
|
||||||
/>
|
|
||||||
<div className="document-card__meta">
|
|
||||||
<div className="document-card__title" title={doc.title}>
|
|
||||||
{correspondents.length > 0 ? (
|
|
||||||
<span className="doc-correspondents">
|
|
||||||
<CorrespondentLinks
|
|
||||||
correspondents={correspondents}
|
|
||||||
activeCorrespondentIdSet={activeCorrespondentIdSet}
|
|
||||||
onCorrespondentClick={onCorrespondentClick}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{isEditingDoc ? (
|
|
||||||
<div className="document-card__title-edit doc-title-edit">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
ref={attachDocumentInputRef}
|
|
||||||
value={documentDraftValue}
|
|
||||||
onChange={(event) => setDocumentDraft(event.target.value)}
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
submitDocumentEditing(doc);
|
|
||||||
} else if (event.key === 'Escape') {
|
|
||||||
event.preventDefault();
|
|
||||||
cancelDocumentEditing(event);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={(event) => {
|
|
||||||
const nextFocus = event.relatedTarget;
|
|
||||||
if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) {
|
|
||||||
cancelDocumentEditing();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="icon-button"
|
|
||||||
aria-label="Save title"
|
|
||||||
title="Save title"
|
|
||||||
disabled={!canSubmitDocument || isDocumentSaving}
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
submitDocumentEditing(doc);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CheckIcon />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="icon-button"
|
|
||||||
aria-label="Cancel"
|
|
||||||
title="Cancel"
|
|
||||||
onClick={(event) => cancelDocumentEditing(event)}
|
|
||||||
>
|
|
||||||
<CloseIcon />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="document-card__title-row">
|
|
||||||
<span
|
|
||||||
className="document-card__title-badge"
|
|
||||||
role={allowInlineDocumentEdit ? 'button' : undefined}
|
|
||||||
tabIndex={allowInlineDocumentEdit ? 0 : undefined}
|
|
||||||
onClick={(event) => {
|
|
||||||
if (!allowInlineDocumentEdit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
beginDocumentEditing(doc);
|
|
||||||
}}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (!allowInlineDocumentEdit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
beginDocumentEditing(doc);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{doc.title}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{visibleTags.length > 0 && (
|
|
||||||
<div className="document-card__tags">
|
|
||||||
{visibleTags.map((tag, index) => {
|
|
||||||
const colorSource = tag?.color || tagLookupById?.get(tag.id)?.color;
|
|
||||||
const style = getTagColorStyle(colorSource);
|
|
||||||
const tagId = tag?.id ?? null;
|
|
||||||
const clickable = tagId != null && typeof onTagClick === 'function';
|
|
||||||
const key = tagId ?? `${doc.id}-tag-${index}`;
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={key}
|
|
||||||
className="badge tag-chip"
|
|
||||||
style={style || undefined}
|
|
||||||
title={tag.label}
|
|
||||||
role={clickable ? 'button' : undefined}
|
|
||||||
onClick={clickable ? (event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
if (tagId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onTagClick?.(tagId);
|
|
||||||
} : undefined}
|
|
||||||
draggable
|
|
||||||
onDragStart={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
try {
|
|
||||||
if (event.dataTransfer) {
|
|
||||||
event.dataTransfer.effectAllowed = 'copyMove';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[documents] Failed to configure drag effect', error);
|
|
||||||
}
|
|
||||||
writeTagTransferData(event.dataTransfer, tag, doc.id);
|
|
||||||
}}
|
|
||||||
onDragEnd={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
onKeyDown={clickable ? (event) => {
|
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
if (tagId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onTagClick?.(tagId);
|
|
||||||
}
|
|
||||||
} : undefined}
|
|
||||||
>
|
|
||||||
{tag.label}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{remainingTagCount > 0 && (
|
|
||||||
<span className="badge tag-chip tag-chip--more">+{remainingTagCount}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DocumentsGrid;
|
|
||||||
@@ -1,449 +0,0 @@
|
|||||||
import React, { useMemo } from 'react';
|
|
||||||
import { FolderIcon, CheckIcon, CloseIcon } from '../ui/icons';
|
|
||||||
import { getTagColorStyle } from '../utils/colors';
|
|
||||||
import { formatDate } from '../utils/date';
|
|
||||||
import DocumentThumbnailImage from './DocumentThumbnailImage';
|
|
||||||
import CorrespondentLinks from './CorrespondentLinks';
|
|
||||||
import { resolveCorrespondents } from './correspondents';
|
|
||||||
import { writeTagTransferData, parseTagTransferPayload } from './tagTransfer';
|
|
||||||
import useInlineRename from './useInlineRename';
|
|
||||||
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
|
|
||||||
|
|
||||||
import type { DocumentsViewProps } from './panel/DocumentsPanel';
|
|
||||||
import type {
|
|
||||||
Document,
|
|
||||||
Folder,
|
|
||||||
} from '../types/documents';
|
|
||||||
|
|
||||||
const DocumentsList: React.FC<DocumentsViewProps> = ({
|
|
||||||
entries,
|
|
||||||
draggingDocumentIdsSet,
|
|
||||||
draggedFolderId,
|
|
||||||
ensureAssetUrl,
|
|
||||||
getDocumentAsset,
|
|
||||||
onFolderClick,
|
|
||||||
onFolderSelect,
|
|
||||||
onFolderDragOver,
|
|
||||||
onFolderDragLeave,
|
|
||||||
onFolderDrop,
|
|
||||||
onFolderDragStart,
|
|
||||||
onFolderDragEnd,
|
|
||||||
onFolderRename,
|
|
||||||
onDocumentClick,
|
|
||||||
onDocumentActivate,
|
|
||||||
onDocumentDragStart,
|
|
||||||
onDocumentDragEnd,
|
|
||||||
onDocumentTagDragOver,
|
|
||||||
onDocumentTagDragLeave,
|
|
||||||
onDocumentTagDrop,
|
|
||||||
onDocumentRename,
|
|
||||||
tagLookupById,
|
|
||||||
onTagClick,
|
|
||||||
onCorrespondentClick,
|
|
||||||
activeCorrespondentIdSet,
|
|
||||||
scrollRef,
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
selectedDocumentIds,
|
|
||||||
selectedFolderIds,
|
|
||||||
clearSelection,
|
|
||||||
} = useWorkspaceSelectionContext();
|
|
||||||
const selectedDocumentIdsSet = useMemo(() => new Set(selectedDocumentIds), [selectedDocumentIds]);
|
|
||||||
const selectedFolderIdsSet = useMemo(
|
|
||||||
() => new Set(selectedFolderIds || []),
|
|
||||||
[selectedFolderIds],
|
|
||||||
);
|
|
||||||
const {
|
|
||||||
editingId: editingDocumentId,
|
|
||||||
draftValue: documentDraft,
|
|
||||||
setDraftValue: setDocumentDraft,
|
|
||||||
beginEditing: beginDocumentEditing,
|
|
||||||
cancelEditing: cancelDocumentEditing,
|
|
||||||
submitEditing: submitDocumentEditing,
|
|
||||||
savingId: savingDocumentId,
|
|
||||||
attachInputRef: attachDocumentInputRef,
|
|
||||||
} = useInlineRename<Document>(onDocumentRename, {
|
|
||||||
getCurrentValue: (doc: Document) => doc?.title ?? '',
|
|
||||||
getEntityId: (doc: Document) => doc?.id ?? null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
editingId: editingFolderId,
|
|
||||||
draftValue: folderDraft,
|
|
||||||
setDraftValue: setFolderDraft,
|
|
||||||
beginEditing: beginFolderEditing,
|
|
||||||
cancelEditing: cancelFolderEditing,
|
|
||||||
submitEditing: submitFolderEditing,
|
|
||||||
savingId: savingFolderId,
|
|
||||||
attachInputRef: attachFolderInputRef,
|
|
||||||
} = useInlineRename<Folder>(onFolderRename, {
|
|
||||||
getCurrentValue: (folder: Folder) => folder?.name ?? '',
|
|
||||||
getEntityId: (folder: Folder) => folder?.id ?? null,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const documentSelectionCount = selectedDocumentIdsSet?.size ?? 0;
|
|
||||||
const folderSelectionCount = selectedFolderIdsSet?.size ?? 0;
|
|
||||||
const totalSelectionCount = documentSelectionCount + folderSelectionCount;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table aria-multiselectable="true">
|
|
||||||
<thead
|
|
||||||
onClick={() => {
|
|
||||||
clearSelection();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<tr>
|
|
||||||
<th> </th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Issued</th>
|
|
||||||
<th>Added</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{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 canRenameFolder = Boolean(onFolderRename) && folder.id !== 'root';
|
|
||||||
const isFolderEditing = editingFolderId === folder.id;
|
|
||||||
const folderDraftValue = isFolderEditing ? folderDraft : folder.name;
|
|
||||||
const trimmedFolderDraft = isFolderEditing ? folderDraft.trim() : '';
|
|
||||||
const isFolderSaving = savingFolderId === folder.id;
|
|
||||||
const canSubmitFolder =
|
|
||||||
isFolderEditing && trimmedFolderDraft.length > 0 && trimmedFolderDraft !== folder.name;
|
|
||||||
const allowInlineFolderEdit = canRenameFolder && isSelectedFolder && totalSelectionCount === 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={entry.key}
|
|
||||||
className={`folder${isDraggingFolder ? ' is-dragging' : ''}${isSelectedFolder ? ' selected' : ''}`}
|
|
||||||
id={`folder-row-${folder.id}`}
|
|
||||||
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)}
|
|
||||||
draggable={canDragFolder}
|
|
||||||
onDragStart={(event) => {
|
|
||||||
if (canDragFolder) {
|
|
||||||
onFolderDragStart?.(event, folder.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDragEnd={(event) => {
|
|
||||||
if (canDragFolder) {
|
|
||||||
onFolderDragEnd?.(event);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<td className="thumb-cell">
|
|
||||||
<div className="thumb-icon">
|
|
||||||
<FolderIcon className="thumb-icon__image" size={32} />
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="doc-list__name">
|
|
||||||
<div className="doc-list__name-content">
|
|
||||||
<span className="doc-name__title">
|
|
||||||
<span className="doc-name__primary">
|
|
||||||
{isFolderEditing ? (
|
|
||||||
<span className="doc-title-edit">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
ref={attachFolderInputRef}
|
|
||||||
value={folderDraftValue}
|
|
||||||
onChange={(event) => setFolderDraft(event.target.value)}
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
submitFolderEditing(folder);
|
|
||||||
} else if (event.key === 'Escape') {
|
|
||||||
event.preventDefault();
|
|
||||||
cancelFolderEditing(event);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={(event) => {
|
|
||||||
const nextFocus = event.relatedTarget;
|
|
||||||
if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) {
|
|
||||||
cancelFolderEditing();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="icon-button"
|
|
||||||
aria-label="Save name"
|
|
||||||
title="Save name"
|
|
||||||
disabled={!canSubmitFolder || isFolderSaving}
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
submitFolderEditing(folder);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CheckIcon />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="icon-button"
|
|
||||||
aria-label="Cancel"
|
|
||||||
title="Cancel"
|
|
||||||
onClick={(event) => {
|
|
||||||
cancelFolderEditing(event);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CloseIcon />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className="doc-name__primary-text"
|
|
||||||
role={allowInlineFolderEdit ? 'button' : undefined}
|
|
||||||
tabIndex={allowInlineFolderEdit ? 0 : undefined}
|
|
||||||
onClick={(event) => {
|
|
||||||
if (!allowInlineFolderEdit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
beginFolderEditing(folder);
|
|
||||||
}}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (!allowInlineFolderEdit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
beginFolderEditing(folder);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{folder.name}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>—</td>
|
|
||||||
<td>—</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const doc = entry.document;
|
|
||||||
if (!doc) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const isSelected = selectedDocumentIdsSet?.has(doc.id);
|
|
||||||
const isDraggingDoc = draggingDocumentIdsSet?.has(doc.id);
|
|
||||||
const rowClasses = ['document'];
|
|
||||||
if (isSelected) rowClasses.push('selected');
|
|
||||||
if (isDraggingDoc) rowClasses.push('is-dragging');
|
|
||||||
const correspondents = resolveCorrespondents(doc);
|
|
||||||
const isEditingDoc = editingDocumentId === doc.id;
|
|
||||||
const documentDraftValue = isEditingDoc ? documentDraft : doc.title;
|
|
||||||
const trimmedDocumentDraft = isEditingDoc ? documentDraft.trim() : '';
|
|
||||||
const isDocumentSaving = savingDocumentId === doc.id;
|
|
||||||
const canSubmitDocument =
|
|
||||||
isEditingDoc && trimmedDocumentDraft.length > 0 && trimmedDocumentDraft !== doc.title;
|
|
||||||
const allowInlineDocumentEdit =
|
|
||||||
onDocumentRename && isSelected && totalSelectionCount === 1;
|
|
||||||
const issuedLabel = formatDate(doc.issued_at);
|
|
||||||
const addedLabel = formatDate(doc.created_at || doc.uploaded_at);
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={entry.key}
|
|
||||||
className={rowClasses.join(' ')}
|
|
||||||
id={`document-row-${doc.id}`}
|
|
||||||
data-doc-id={doc.id}
|
|
||||||
onClick={(event) => onDocumentClick?.(doc, event)}
|
|
||||||
onDoubleClick={(event) => onDocumentActivate?.(doc, event)}
|
|
||||||
onDragStart={(event) => onDocumentDragStart?.(event, doc)}
|
|
||||||
onDragEnd={(event) => onDocumentDragEnd?.(event)}
|
|
||||||
onDragOver={onDocumentTagDragOver}
|
|
||||||
onDragLeave={onDocumentTagDragLeave}
|
|
||||||
onDrop={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
const payload = parseTagTransferPayload(event);
|
|
||||||
if (payload && onDocumentTagDrop) {
|
|
||||||
onDocumentTagDrop(doc.id, payload);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<td className="thumb-cell">
|
|
||||||
<DocumentThumbnailImage
|
|
||||||
document={doc}
|
|
||||||
ensureAssetUrl={ensureAssetUrl}
|
|
||||||
getDocumentAsset={getDocumentAsset}
|
|
||||||
alt={`Thumbnail for ${doc.title}`}
|
|
||||||
scrollRootRef={scrollRef}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className="doc-list__name">
|
|
||||||
<div className="doc-name">
|
|
||||||
<div className="doc-list__name-content">
|
|
||||||
<span className="doc-name__title">
|
|
||||||
{correspondents.length > 0 ? (
|
|
||||||
<span className="doc-correspondents">
|
|
||||||
<CorrespondentLinks
|
|
||||||
correspondents={correspondents}
|
|
||||||
activeCorrespondentIdSet={activeCorrespondentIdSet}
|
|
||||||
onCorrespondentClick={onCorrespondentClick}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
<span className="doc-name__primary">
|
|
||||||
{isEditingDoc ? (
|
|
||||||
<span className="doc-title-edit">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
ref={attachDocumentInputRef}
|
|
||||||
value={documentDraftValue}
|
|
||||||
onChange={(event) => setDocumentDraft(event.target.value)}
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
submitDocumentEditing(doc);
|
|
||||||
} else if (event.key === 'Escape') {
|
|
||||||
event.preventDefault();
|
|
||||||
cancelDocumentEditing(event);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={(event) => {
|
|
||||||
const nextFocus = event.relatedTarget;
|
|
||||||
if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) {
|
|
||||||
cancelDocumentEditing();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="icon-button"
|
|
||||||
aria-label="Save title"
|
|
||||||
title="Save title"
|
|
||||||
disabled={!canSubmitDocument || isDocumentSaving}
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
submitDocumentEditing(doc);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CheckIcon />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="icon-button"
|
|
||||||
aria-label="Cancel"
|
|
||||||
title="Cancel"
|
|
||||||
onClick={(event) => {
|
|
||||||
cancelDocumentEditing(event);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CloseIcon />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className="doc-name__primary-text"
|
|
||||||
role={allowInlineDocumentEdit ? 'button' : undefined}
|
|
||||||
tabIndex={allowInlineDocumentEdit ? 0 : undefined}
|
|
||||||
onClick={(event) => {
|
|
||||||
if (!allowInlineDocumentEdit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
beginDocumentEditing(doc);
|
|
||||||
}}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (!allowInlineDocumentEdit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
beginDocumentEditing(doc);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{doc.title}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{(doc.tags || []).length > 0 && (
|
|
||||||
<div className="doc-name__tags">
|
|
||||||
{(doc.tags || []).map((tag, index) => {
|
|
||||||
const colorSource = tag?.color || tagLookupById?.get(tag.id)?.color;
|
|
||||||
const style = getTagColorStyle(colorSource);
|
|
||||||
const tagId = tag?.id ?? null;
|
|
||||||
const clickable = tagId != null && typeof onTagClick === 'function';
|
|
||||||
const key = tagId ?? `${doc.id}-tag-${index}`;
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={key}
|
|
||||||
className="badge tag-chip"
|
|
||||||
style={style || undefined}
|
|
||||||
title={tag.label}
|
|
||||||
role={clickable ? 'button' : undefined}
|
|
||||||
onClick={clickable ? (event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
if (tagId == null) return;
|
|
||||||
onTagClick?.(tagId);
|
|
||||||
} : undefined}
|
|
||||||
draggable
|
|
||||||
onDragStart={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
try {
|
|
||||||
if (event.dataTransfer) {
|
|
||||||
event.dataTransfer.effectAllowed = 'copyMove';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[documents] Failed to configure drag effect', error);
|
|
||||||
}
|
|
||||||
writeTagTransferData(event.dataTransfer, tag, doc.id);
|
|
||||||
}}
|
|
||||||
onDragEnd={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
onKeyDown={clickable ? (event) => {
|
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
if (tagId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onTagClick?.(tagId);
|
|
||||||
}
|
|
||||||
} : undefined}
|
|
||||||
>
|
|
||||||
{tag.label}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>{issuedLabel}</td>
|
|
||||||
<td>{addedLabel}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DocumentsList;
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useDocumentViewLogic, DocumentViewLogic } from './hooks/useDocumentViewLogic';
|
||||||
|
import DocumentsListRow from './components/DocumentsListRow';
|
||||||
|
import DocumentsGridCard from './components/DocumentsGridCard';
|
||||||
|
import DocumentsListContainer from './components/DocumentsListContainer';
|
||||||
|
import DocumentsGridContainer from './components/DocumentsGridContainer';
|
||||||
|
import type { DocumentsViewProps } from './panel/DocumentsPanel';
|
||||||
|
|
||||||
|
interface AbstractDocumentsViewProps<CProps extends { clearSelection: () => void; children: React.ReactNode }> extends DocumentsViewProps {
|
||||||
|
ContainerComponent: React.ComponentType<CProps>;
|
||||||
|
ItemComponent: React.ComponentType<{ entry: any; viewLogic: DocumentViewLogic } & DocumentsViewProps>;
|
||||||
|
containerProps?: Omit<CProps, 'children' | 'clearSelection'>;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AbstractDocumentsView = <CProps extends { clearSelection: () => void; children: React.ReactNode }>({
|
||||||
|
ContainerComponent,
|
||||||
|
ItemComponent,
|
||||||
|
containerProps,
|
||||||
|
...props
|
||||||
|
}: AbstractDocumentsViewProps<CProps>) => {
|
||||||
|
const { entries, onDocumentRename, onFolderRename } = props;
|
||||||
|
const viewLogic = useDocumentViewLogic({
|
||||||
|
onDocumentRename,
|
||||||
|
onFolderRename,
|
||||||
|
});
|
||||||
|
const { clearSelection } = viewLogic;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContainerComponent clearSelection={clearSelection} {...(containerProps as any)}>
|
||||||
|
{entries.map((entry) => (
|
||||||
|
<ItemComponent
|
||||||
|
key={entry.key}
|
||||||
|
entry={entry}
|
||||||
|
viewLogic={viewLogic}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ContainerComponent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DocumentsList: React.FC<DocumentsViewProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<AbstractDocumentsView
|
||||||
|
ContainerComponent={DocumentsListContainer}
|
||||||
|
ItemComponent={DocumentsListRow}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DocumentsGrid: React.FC<DocumentsViewProps & { gridIconSize?: number }> = (props) => {
|
||||||
|
return (
|
||||||
|
<AbstractDocumentsView
|
||||||
|
ContainerComponent={DocumentsGridContainer}
|
||||||
|
ItemComponent={DocumentsGridCard}
|
||||||
|
containerProps={{ gridIconSize: props.gridIconSize }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { DocumentsViewProps } from '../panel/DocumentsPanel';
|
||||||
|
import type { DocumentViewLogic } from '../hooks/useDocumentViewLogic';
|
||||||
|
import { useDocumentItemLogic } from '../hooks/useDocumentItemLogic';
|
||||||
|
import EntryShell from './EntryShell';
|
||||||
|
|
||||||
|
interface DocumentEntryProps extends DocumentsViewProps {
|
||||||
|
doc: any;
|
||||||
|
viewLogic: DocumentViewLogic;
|
||||||
|
component: React.ElementType;
|
||||||
|
className?: string;
|
||||||
|
role?: string;
|
||||||
|
children: (logic: ReturnType<typeof useDocumentItemLogic>) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DocumentEntry: React.FC<DocumentEntryProps> = (props) => {
|
||||||
|
const { doc, component, className, role, children, viewLogic } = props;
|
||||||
|
const logic = useDocumentItemLogic({ doc, viewLogic, ...props });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EntryShell
|
||||||
|
component={component}
|
||||||
|
id={`document-${doc.id}`}
|
||||||
|
docId={doc.id}
|
||||||
|
handlers={logic.handlers}
|
||||||
|
isSelected={logic.isSelected}
|
||||||
|
isDragging={logic.isDraggingDoc}
|
||||||
|
canDrag={true}
|
||||||
|
className={className}
|
||||||
|
role={role}
|
||||||
|
>
|
||||||
|
{children(logic)}
|
||||||
|
</EntryShell>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentEntry;
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { getTagColorStyle } from '../../utils/colors';
|
||||||
|
import { writeTagTransferData } from '../tagTransfer';
|
||||||
|
import type { DocumentTag } from '../../types/documents';
|
||||||
|
import type { Identifier } from '../../types/identifiers';
|
||||||
|
|
||||||
|
interface DocumentTagsProps {
|
||||||
|
tags: DocumentTag[];
|
||||||
|
tagLookupById?: Map<Identifier, DocumentTag> | null;
|
||||||
|
onTagClick?: (tagId: Identifier) => void;
|
||||||
|
docId: Identifier;
|
||||||
|
maxTags?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DocumentTags: React.FC<DocumentTagsProps> = ({
|
||||||
|
tags,
|
||||||
|
tagLookupById,
|
||||||
|
onTagClick,
|
||||||
|
docId,
|
||||||
|
maxTags,
|
||||||
|
}) => {
|
||||||
|
const visibleTags = maxTags ? tags.slice(0, maxTags) : tags;
|
||||||
|
const remainingTagCount = maxTags && tags.length > maxTags ? tags.length - maxTags : 0;
|
||||||
|
|
||||||
|
if (tags.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{visibleTags.map((tag, index) => {
|
||||||
|
const colorSource = tag?.color || tagLookupById?.get(tag.id)?.color;
|
||||||
|
const style = getTagColorStyle(colorSource);
|
||||||
|
const tagId = tag?.id ?? null;
|
||||||
|
const clickable = tagId != null && typeof onTagClick === 'function';
|
||||||
|
const key = tagId ?? `${docId}-tag-${index}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={key}
|
||||||
|
className="badge tag-chip"
|
||||||
|
style={style || undefined}
|
||||||
|
title={tag.label}
|
||||||
|
role={clickable ? 'button' : undefined}
|
||||||
|
onClick={clickable ? (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (tagId == null) return;
|
||||||
|
onTagClick?.(tagId);
|
||||||
|
} : undefined}
|
||||||
|
draggable
|
||||||
|
onDragStart={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
try {
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.effectAllowed = 'copyMove';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[documents] Failed to configure drag effect', error);
|
||||||
|
}
|
||||||
|
writeTagTransferData(event.dataTransfer, tag, docId);
|
||||||
|
}}
|
||||||
|
onDragEnd={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
onKeyDown={clickable ? (event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (tagId == null) return;
|
||||||
|
onTagClick?.(tagId);
|
||||||
|
}
|
||||||
|
} : undefined}
|
||||||
|
>
|
||||||
|
{tag.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{remainingTagCount > 0 && (
|
||||||
|
<span className="badge tag-chip tag-chip--more">+{remainingTagCount}</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentTags;
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FolderIcon } from '../../ui/icons';
|
||||||
|
import DocumentThumbnailImage from '../DocumentThumbnailImage';
|
||||||
|
import { resolveCorrespondents } from '../correspondents';
|
||||||
|
import type { DocumentsListEntry } from '../../types/documents';
|
||||||
|
import type { DocumentsViewProps } from '../panel/DocumentsPanel';
|
||||||
|
import type { DocumentViewLogic } from '../hooks/useDocumentViewLogic';
|
||||||
|
import EditableEntryTitle from './EditableEntryTitle';
|
||||||
|
import EntryCorrespondents from './EntryCorrespondents';
|
||||||
|
import EntryTags from './EntryTags';
|
||||||
|
import FolderEntry from './FolderEntry';
|
||||||
|
import DocumentEntry from './DocumentEntry';
|
||||||
|
|
||||||
|
interface DocumentsGridCardProps extends DocumentsViewProps {
|
||||||
|
entry: DocumentsListEntry;
|
||||||
|
viewLogic: DocumentViewLogic;
|
||||||
|
gridIconSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => {
|
||||||
|
const { entry, gridIconSize } = props;
|
||||||
|
|
||||||
|
if (entry.type === 'folder') {
|
||||||
|
const folder = entry.folder;
|
||||||
|
if (!folder) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FolderEntry
|
||||||
|
{...props}
|
||||||
|
folder={folder}
|
||||||
|
component="div"
|
||||||
|
className="document-card folder-card"
|
||||||
|
role="listitem"
|
||||||
|
>
|
||||||
|
{(logic) => (
|
||||||
|
<>
|
||||||
|
<div className="folder-card__icon">
|
||||||
|
<FolderIcon className="folder-card__icon-svg" size={gridIconSize} />
|
||||||
|
</div>
|
||||||
|
<div className="folder-card__meta">
|
||||||
|
<div className="folder-card__label-row">
|
||||||
|
<EditableEntryTitle
|
||||||
|
isEditing={logic.isFolderEditing}
|
||||||
|
draftValue={logic.folderDraftValue}
|
||||||
|
onChange={logic.handlers.onRenameChange}
|
||||||
|
onSubmit={logic.handlers.onRenameSubmit}
|
||||||
|
onCancel={logic.handlers.onRenameCancel}
|
||||||
|
isSaving={logic.isFolderSaving}
|
||||||
|
canSubmit={logic.canSubmitFolder}
|
||||||
|
inputRef={logic.attachFolderInputRef}
|
||||||
|
allowInlineEdit={logic.allowInlineFolderEdit}
|
||||||
|
onBeginEditing={logic.handlers.onRenameBegin}
|
||||||
|
className="folder-card__name"
|
||||||
|
>
|
||||||
|
{folder.name}
|
||||||
|
</EditableEntryTitle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</FolderEntry>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = entry.document;
|
||||||
|
if (!doc) return null;
|
||||||
|
|
||||||
|
const correspondents = resolveCorrespondents(doc);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocumentEntry
|
||||||
|
{...props}
|
||||||
|
doc={doc}
|
||||||
|
component="div"
|
||||||
|
className="document-card document"
|
||||||
|
role="listitem"
|
||||||
|
>
|
||||||
|
{(logic) => (
|
||||||
|
<>
|
||||||
|
<DocumentThumbnailImage
|
||||||
|
document={doc}
|
||||||
|
ensureAssetUrl={props.ensureAssetUrl}
|
||||||
|
getDocumentAsset={props.getDocumentAsset}
|
||||||
|
alt={`Thumbnail for ${doc.title}`}
|
||||||
|
maxSize={gridIconSize}
|
||||||
|
scrollRootRef={props.scrollRef}
|
||||||
|
/>
|
||||||
|
<div className="document-card__meta">
|
||||||
|
<div className="document-card__title" title={doc.title}>
|
||||||
|
<EntryCorrespondents
|
||||||
|
correspondents={correspondents}
|
||||||
|
activeCorrespondentIdSet={props.activeCorrespondentIdSet}
|
||||||
|
onCorrespondentClick={props.onCorrespondentClick}
|
||||||
|
/>
|
||||||
|
<div className="document-card__title-row">
|
||||||
|
<EditableEntryTitle
|
||||||
|
isEditing={logic.isEditingDoc}
|
||||||
|
draftValue={logic.documentDraftValue}
|
||||||
|
onChange={logic.handlers.onRenameChange}
|
||||||
|
onSubmit={logic.handlers.onRenameSubmit}
|
||||||
|
onCancel={logic.handlers.onRenameCancel}
|
||||||
|
isSaving={logic.isDocumentSaving}
|
||||||
|
canSubmit={logic.canSubmitDocument}
|
||||||
|
inputRef={logic.attachDocumentInputRef}
|
||||||
|
allowInlineEdit={logic.allowInlineDocumentEdit}
|
||||||
|
onBeginEditing={logic.handlers.onRenameBegin}
|
||||||
|
className="document-card__title-badge"
|
||||||
|
>
|
||||||
|
{doc.title}
|
||||||
|
</EditableEntryTitle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="document-card__tags">
|
||||||
|
<EntryTags
|
||||||
|
tags={doc.tags || []}
|
||||||
|
tagLookupById={props.tagLookupById}
|
||||||
|
onTagClick={props.onTagClick}
|
||||||
|
docId={doc.id}
|
||||||
|
maxTags={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DocumentEntry>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentsGridCard;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface DocumentsGridContainerProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
clearSelection: () => void;
|
||||||
|
gridIconSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DocumentsGridContainer: React.FC<DocumentsGridContainerProps> = ({
|
||||||
|
children,
|
||||||
|
clearSelection,
|
||||||
|
gridIconSize,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="documents-grid"
|
||||||
|
role="list"
|
||||||
|
style={
|
||||||
|
gridIconSize
|
||||||
|
? ({ '--documents-grid-icon-size': `${gridIconSize}px` } as React.CSSProperties)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onClick={(event) => {
|
||||||
|
if (event.target === event.currentTarget) {
|
||||||
|
clearSelection();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentsGridContainer;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface DocumentsListContainerProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
clearSelection: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DocumentsListContainer: React.FC<DocumentsListContainerProps> = ({
|
||||||
|
children,
|
||||||
|
clearSelection,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<table aria-multiselectable="true">
|
||||||
|
<thead
|
||||||
|
onClick={() => {
|
||||||
|
clearSelection();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th> </th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Issued</th>
|
||||||
|
<th>Added</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{children}</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentsListContainer;
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FolderIcon } from '../../ui/icons';
|
||||||
|
import { formatDate } from '../../utils/date';
|
||||||
|
import DocumentThumbnailImage from '../DocumentThumbnailImage';
|
||||||
|
import { resolveCorrespondents } from '../correspondents';
|
||||||
|
import type { DocumentsListEntry } from '../../types/documents';
|
||||||
|
import type { DocumentsViewProps } from '../panel/DocumentsPanel';
|
||||||
|
import type { DocumentViewLogic } from '../hooks/useDocumentViewLogic';
|
||||||
|
import EditableEntryTitle from './EditableEntryTitle';
|
||||||
|
import EntryCorrespondents from './EntryCorrespondents';
|
||||||
|
import EntryTags from './EntryTags';
|
||||||
|
import FolderEntry from './FolderEntry';
|
||||||
|
import DocumentEntry from './DocumentEntry';
|
||||||
|
|
||||||
|
interface DocumentsListRowProps extends DocumentsViewProps {
|
||||||
|
entry: DocumentsListEntry;
|
||||||
|
viewLogic: DocumentViewLogic;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
|
||||||
|
const { entry } = props;
|
||||||
|
|
||||||
|
if (entry.type === 'folder') {
|
||||||
|
const folder = entry.folder;
|
||||||
|
if (!folder) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FolderEntry
|
||||||
|
{...props}
|
||||||
|
folder={folder}
|
||||||
|
component="tr"
|
||||||
|
className="folder"
|
||||||
|
>
|
||||||
|
{(logic) => (
|
||||||
|
<>
|
||||||
|
<td className="thumb-cell">
|
||||||
|
<div className="thumb-icon">
|
||||||
|
<FolderIcon className="thumb-icon__image" size={32} />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="doc-list__name">
|
||||||
|
<div className="doc-list__name-content">
|
||||||
|
<span className="doc-name__title">
|
||||||
|
<span className="doc-name__primary">
|
||||||
|
<EditableEntryTitle
|
||||||
|
isEditing={logic.isFolderEditing}
|
||||||
|
draftValue={logic.folderDraftValue}
|
||||||
|
onChange={logic.handlers.onRenameChange}
|
||||||
|
onSubmit={logic.handlers.onRenameSubmit}
|
||||||
|
onCancel={logic.handlers.onRenameCancel}
|
||||||
|
isSaving={logic.isFolderSaving}
|
||||||
|
canSubmit={logic.canSubmitFolder}
|
||||||
|
inputRef={logic.attachFolderInputRef}
|
||||||
|
allowInlineEdit={logic.allowInlineFolderEdit}
|
||||||
|
onBeginEditing={logic.handlers.onRenameBegin}
|
||||||
|
className="doc-name__primary-text"
|
||||||
|
>
|
||||||
|
{folder.name}
|
||||||
|
</EditableEntryTitle>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>—</td>
|
||||||
|
<td>—</td>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</FolderEntry>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = entry.document;
|
||||||
|
if (!doc) return null;
|
||||||
|
|
||||||
|
const correspondents = resolveCorrespondents(doc);
|
||||||
|
const issuedLabel = formatDate(doc.issued_at);
|
||||||
|
const addedLabel = formatDate(doc.created_at || doc.uploaded_at);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocumentEntry
|
||||||
|
{...props}
|
||||||
|
doc={doc}
|
||||||
|
component="tr"
|
||||||
|
className="document"
|
||||||
|
>
|
||||||
|
{(logic) => (
|
||||||
|
<>
|
||||||
|
<td className="thumb-cell">
|
||||||
|
<DocumentThumbnailImage
|
||||||
|
document={doc}
|
||||||
|
ensureAssetUrl={props.ensureAssetUrl}
|
||||||
|
getDocumentAsset={props.getDocumentAsset}
|
||||||
|
alt={`Thumbnail for ${doc.title}`}
|
||||||
|
scrollRootRef={props.scrollRef}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="doc-list__name">
|
||||||
|
<div className="doc-name">
|
||||||
|
<div className="doc-list__name-content">
|
||||||
|
<span className="doc-name__title">
|
||||||
|
<EntryCorrespondents
|
||||||
|
correspondents={correspondents}
|
||||||
|
activeCorrespondentIdSet={props.activeCorrespondentIdSet}
|
||||||
|
onCorrespondentClick={props.onCorrespondentClick}
|
||||||
|
/>
|
||||||
|
<span className="doc-name__primary">
|
||||||
|
<EditableEntryTitle
|
||||||
|
isEditing={logic.isEditingDoc}
|
||||||
|
draftValue={logic.documentDraftValue}
|
||||||
|
onChange={logic.handlers.onRenameChange}
|
||||||
|
onSubmit={logic.handlers.onRenameSubmit}
|
||||||
|
onCancel={logic.handlers.onRenameCancel}
|
||||||
|
isSaving={logic.isDocumentSaving}
|
||||||
|
canSubmit={logic.canSubmitDocument}
|
||||||
|
inputRef={logic.attachDocumentInputRef}
|
||||||
|
allowInlineEdit={logic.allowInlineDocumentEdit}
|
||||||
|
onBeginEditing={logic.handlers.onRenameBegin}
|
||||||
|
className="doc-name__primary-text"
|
||||||
|
>
|
||||||
|
{doc.title}
|
||||||
|
</EditableEntryTitle>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="doc-name__tags">
|
||||||
|
<EntryTags
|
||||||
|
tags={doc.tags || []}
|
||||||
|
tagLookupById={props.tagLookupById}
|
||||||
|
onTagClick={props.onTagClick}
|
||||||
|
docId={doc.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{issuedLabel}</td>
|
||||||
|
<td>{addedLabel}</td>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DocumentEntry>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentsListRow;
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import InlineRenameInput from './InlineRenameInput';
|
||||||
|
|
||||||
|
interface EditableEntryTitleProps {
|
||||||
|
isEditing: boolean;
|
||||||
|
draftValue: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
onCancel: (event?: React.SyntheticEvent) => void;
|
||||||
|
isSaving: boolean;
|
||||||
|
canSubmit: boolean;
|
||||||
|
inputRef: (ref: HTMLInputElement | null) => void;
|
||||||
|
allowInlineEdit: boolean | undefined;
|
||||||
|
onBeginEditing: (event: React.SyntheticEvent) => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditableEntryTitle: React.FC<EditableEntryTitleProps> = ({
|
||||||
|
isEditing,
|
||||||
|
draftValue,
|
||||||
|
onChange,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
isSaving,
|
||||||
|
canSubmit,
|
||||||
|
inputRef,
|
||||||
|
allowInlineEdit,
|
||||||
|
onBeginEditing,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<div className={`doc-title-edit ${className || ''}`}>
|
||||||
|
<InlineRenameInput
|
||||||
|
value={draftValue}
|
||||||
|
onChange={onChange}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onCancel={onCancel}
|
||||||
|
isSaving={isSaving}
|
||||||
|
canSubmit={canSubmit}
|
||||||
|
inputRef={inputRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={className}
|
||||||
|
role={allowInlineEdit ? 'button' : undefined}
|
||||||
|
tabIndex={allowInlineEdit ? 0 : undefined}
|
||||||
|
onClick={onBeginEditing}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (!allowInlineEdit) return;
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
onBeginEditing(event);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditableEntryTitle;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import CorrespondentLinks from '../CorrespondentLinks';
|
||||||
|
import type { Identifier } from '../../types/identifiers';
|
||||||
|
|
||||||
|
interface EntryCorrespondentsProps {
|
||||||
|
correspondents: any[];
|
||||||
|
activeCorrespondentIdSet?: Set<Identifier> | null;
|
||||||
|
onCorrespondentClick?: (correspondentId: Identifier) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntryCorrespondents: React.FC<EntryCorrespondentsProps> = (props) => {
|
||||||
|
if (!props.correspondents || props.correspondents.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="doc-correspondents">
|
||||||
|
<CorrespondentLinks
|
||||||
|
correspondents={props.correspondents}
|
||||||
|
activeCorrespondentIdSet={props.activeCorrespondentIdSet}
|
||||||
|
onCorrespondentClick={props.onCorrespondentClick}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntryCorrespondents;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import React, { type DragEvent } from 'react';
|
||||||
|
|
||||||
|
export interface EntryShellHandlers {
|
||||||
|
onClick: (event: React.MouseEvent) => void;
|
||||||
|
onDoubleClick: (event: React.MouseEvent) => void;
|
||||||
|
onDragStart: (event: DragEvent<HTMLElement>) => void;
|
||||||
|
onDragEnd: (event: DragEvent<HTMLElement>) => void;
|
||||||
|
onDragOver: (event: DragEvent<HTMLElement>) => void;
|
||||||
|
onDragLeave: (event: DragEvent<HTMLElement>) => void;
|
||||||
|
onDrop: (event: DragEvent<HTMLElement>) => void;
|
||||||
|
onDragOverCapture?: (event: DragEvent<HTMLElement>) => void;
|
||||||
|
onDragLeaveCapture?: (event: DragEvent<HTMLElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntryShellProps {
|
||||||
|
component: React.ElementType;
|
||||||
|
handlers: EntryShellHandlers;
|
||||||
|
isSelected?: boolean;
|
||||||
|
isDragging?: boolean;
|
||||||
|
canDrag?: boolean;
|
||||||
|
id: string;
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
docId?: number;
|
||||||
|
role?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntryShell: React.FC<EntryShellProps> = ({
|
||||||
|
component: Component,
|
||||||
|
handlers,
|
||||||
|
isSelected,
|
||||||
|
isDragging,
|
||||||
|
canDrag,
|
||||||
|
id,
|
||||||
|
className = '',
|
||||||
|
children,
|
||||||
|
docId,
|
||||||
|
role,
|
||||||
|
}) => {
|
||||||
|
const classes = [className];
|
||||||
|
if (isSelected) classes.push('selected');
|
||||||
|
if (isDragging) classes.push('is-dragging');
|
||||||
|
|
||||||
|
const commonProps = {
|
||||||
|
id,
|
||||||
|
className: classes.join(' '),
|
||||||
|
onClick: handlers.onClick,
|
||||||
|
onDoubleClick: handlers.onDoubleClick,
|
||||||
|
draggable: canDrag,
|
||||||
|
onDragStart: handlers.onDragStart,
|
||||||
|
onDragEnd: handlers.onDragEnd,
|
||||||
|
onDragOver: handlers.onDragOver,
|
||||||
|
onDragLeave: handlers.onDragLeave,
|
||||||
|
onDrop: handlers.onDrop,
|
||||||
|
...(docId ? { 'data-doc-id': docId } : {}),
|
||||||
|
...(role ? { role } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component {...commonProps}>
|
||||||
|
{children}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntryShell;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import DocumentTags from './DocumentTags';
|
||||||
|
import type { Identifier } from '../../types/identifiers';
|
||||||
|
import type { DocumentTag } from '../../types/documents';
|
||||||
|
|
||||||
|
interface EntryTagsProps {
|
||||||
|
tags: Identifier[];
|
||||||
|
tagLookupById?: Map<Identifier, DocumentTag> | null;
|
||||||
|
onTagClick?: (tagId: Identifier) => void;
|
||||||
|
docId: Identifier;
|
||||||
|
maxTags?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntryTags: React.FC<EntryTagsProps> = (props) => {
|
||||||
|
if (!props.tags || props.tags.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocumentTags
|
||||||
|
tags={props.tags}
|
||||||
|
tagLookupById={props.tagLookupById}
|
||||||
|
onTagClick={props.onTagClick}
|
||||||
|
docId={props.docId}
|
||||||
|
maxTags={props.maxTags}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntryTags;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { DocumentsViewProps } from '../panel/DocumentsPanel';
|
||||||
|
import type { DocumentViewLogic } from '../hooks/useDocumentViewLogic';
|
||||||
|
import { useFolderItemLogic } from '../hooks/useFolderItemLogic';
|
||||||
|
import EntryShell from './EntryShell';
|
||||||
|
|
||||||
|
interface FolderEntryProps extends DocumentsViewProps {
|
||||||
|
folder: any;
|
||||||
|
viewLogic: DocumentViewLogic;
|
||||||
|
component: React.ElementType;
|
||||||
|
className?: string;
|
||||||
|
role?: string;
|
||||||
|
children: (logic: ReturnType<typeof useFolderItemLogic>) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FolderEntry: React.FC<FolderEntryProps> = (props) => {
|
||||||
|
const { folder, component, className, role, children, viewLogic } = props;
|
||||||
|
const logic = useFolderItemLogic({ folder, viewLogic, ...props });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EntryShell
|
||||||
|
component={component}
|
||||||
|
id={`folder-${folder.id}`}
|
||||||
|
handlers={logic.handlers}
|
||||||
|
isSelected={logic.isSelectedFolder}
|
||||||
|
isDragging={logic.isDraggingFolder}
|
||||||
|
canDrag={logic.canDragFolder}
|
||||||
|
className={className}
|
||||||
|
role={role}
|
||||||
|
>
|
||||||
|
{children(logic)}
|
||||||
|
</EntryShell>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FolderEntry;
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CheckIcon, CloseIcon } from '../../ui/icons';
|
||||||
|
|
||||||
|
interface InlineRenameInputProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
onCancel: (event?: React.SyntheticEvent) => void;
|
||||||
|
isSaving?: boolean;
|
||||||
|
canSubmit?: boolean;
|
||||||
|
inputRef?: React.Ref<HTMLInputElement>;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InlineRenameInput: React.FC<InlineRenameInputProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
isSaving = false,
|
||||||
|
canSubmit = true,
|
||||||
|
inputRef,
|
||||||
|
className = 'doc-title-edit',
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<span className={className}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
ref={inputRef}
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
onSubmit();
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
onCancel(event);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={(event) => {
|
||||||
|
const nextFocus = event.relatedTarget;
|
||||||
|
if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="icon-button"
|
||||||
|
aria-label="Save"
|
||||||
|
title="Save"
|
||||||
|
disabled={!canSubmit || isSaving}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="icon-button"
|
||||||
|
aria-label="Cancel"
|
||||||
|
title="Cancel"
|
||||||
|
onClick={(event) => {
|
||||||
|
onCancel(event);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InlineRenameInput;
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import React, { type DragEvent } from 'react';
|
||||||
|
import { parseTagTransferPayload } from '../tagTransfer';
|
||||||
|
import type { Document } from '../../types/documents';
|
||||||
|
import type { DocumentsViewProps } from '../panel/DocumentsPanel';
|
||||||
|
import type { DocumentViewLogic } from './useDocumentViewLogic';
|
||||||
|
|
||||||
|
interface UseDocumentItemLogicProps extends DocumentsViewProps {
|
||||||
|
doc: Document;
|
||||||
|
viewLogic: DocumentViewLogic;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDocumentItemLogic = (props: UseDocumentItemLogicProps) => {
|
||||||
|
const {
|
||||||
|
doc,
|
||||||
|
viewLogic,
|
||||||
|
draggingDocumentIdsSet,
|
||||||
|
onDocumentClick,
|
||||||
|
onDocumentActivate,
|
||||||
|
onDocumentDragStart,
|
||||||
|
onDocumentDragEnd,
|
||||||
|
onDocumentTagDragOver,
|
||||||
|
onDocumentTagDragLeave,
|
||||||
|
onDocumentTagDrop,
|
||||||
|
onDocumentRename,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedDocumentIdsSet,
|
||||||
|
totalSelectionCount,
|
||||||
|
documentRename: {
|
||||||
|
editingId: editingDocumentId,
|
||||||
|
draftValue: documentDraft,
|
||||||
|
setDraftValue: setDocumentDraft,
|
||||||
|
beginEditing: beginDocumentEditing,
|
||||||
|
cancelEditing: cancelDocumentEditing,
|
||||||
|
submitEditing: submitDocumentEditing,
|
||||||
|
savingId: savingDocumentId,
|
||||||
|
attachInputRef: attachDocumentInputRef,
|
||||||
|
},
|
||||||
|
} = viewLogic;
|
||||||
|
|
||||||
|
const isSelected = selectedDocumentIdsSet?.has(doc.id);
|
||||||
|
const isDraggingDoc = draggingDocumentIdsSet?.has(doc.id);
|
||||||
|
const isEditingDoc = editingDocumentId === doc.id;
|
||||||
|
const documentDraftValue = isEditingDoc ? documentDraft : doc.title;
|
||||||
|
const trimmedDocumentDraft = isEditingDoc ? documentDraft.trim() : '';
|
||||||
|
const isDocumentSaving = savingDocumentId === doc.id;
|
||||||
|
const canSubmitDocument =
|
||||||
|
isEditingDoc && trimmedDocumentDraft.length > 0 && trimmedDocumentDraft !== doc.title;
|
||||||
|
const allowInlineDocumentEdit = onDocumentRename && isSelected && totalSelectionCount === 1;
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
onClick: (event: React.MouseEvent) => onDocumentClick?.(doc, event),
|
||||||
|
onDoubleClick: (event: React.MouseEvent) => onDocumentActivate?.(doc, event),
|
||||||
|
onDragStart: (event: DragEvent<HTMLElement>) => onDocumentDragStart?.(event, doc),
|
||||||
|
onDragEnd: (event: DragEvent<HTMLElement>) => onDocumentDragEnd?.(event),
|
||||||
|
onDragOver: (event: DragEvent<HTMLElement>) => onDocumentTagDragOver?.(event),
|
||||||
|
onDragLeave: onDocumentTagDragLeave,
|
||||||
|
onDrop: (event: DragEvent<HTMLElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const payload = parseTagTransferPayload(event);
|
||||||
|
if (payload && onDocumentTagDrop) {
|
||||||
|
onDocumentTagDrop(doc.id, payload);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRenameChange: setDocumentDraft,
|
||||||
|
onRenameSubmit: () => submitDocumentEditing(doc),
|
||||||
|
onRenameCancel: (event?: React.SyntheticEvent) => cancelDocumentEditing(event),
|
||||||
|
onRenameBegin: (event: React.SyntheticEvent) => {
|
||||||
|
if (!allowInlineDocumentEdit) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
beginDocumentEditing(doc);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSelected,
|
||||||
|
isDraggingDoc,
|
||||||
|
isEditingDoc,
|
||||||
|
documentDraftValue,
|
||||||
|
isDocumentSaving,
|
||||||
|
canSubmitDocument,
|
||||||
|
allowInlineDocumentEdit,
|
||||||
|
attachDocumentInputRef,
|
||||||
|
handlers,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useWorkspaceSelectionContext } from '../../app/WorkspaceSelectionContext';
|
||||||
|
import useInlineRename from '../useInlineRename';
|
||||||
|
import type { Document, Folder } from '../../types/documents';
|
||||||
|
|
||||||
|
interface UseDocumentViewLogicProps {
|
||||||
|
onDocumentRename?: (id: string, name: string) => Promise<boolean> | boolean;
|
||||||
|
onFolderRename?: (id: string, name: string) => Promise<boolean> | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDocumentViewLogic = ({
|
||||||
|
onDocumentRename,
|
||||||
|
onFolderRename,
|
||||||
|
}: UseDocumentViewLogicProps) => {
|
||||||
|
const {
|
||||||
|
selectedDocumentIds,
|
||||||
|
selectedFolderIds,
|
||||||
|
clearSelection,
|
||||||
|
} = useWorkspaceSelectionContext();
|
||||||
|
|
||||||
|
const selectedDocumentIdsSet = useMemo(() => new Set(selectedDocumentIds), [selectedDocumentIds]);
|
||||||
|
const selectedFolderIdsSet = useMemo(
|
||||||
|
() => new Set(selectedFolderIds || []),
|
||||||
|
[selectedFolderIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const documentSelectionCount = selectedDocumentIdsSet.size;
|
||||||
|
const folderSelectionCount = selectedFolderIdsSet.size;
|
||||||
|
const totalSelectionCount = documentSelectionCount + folderSelectionCount;
|
||||||
|
|
||||||
|
const documentRename = useInlineRename<Document>(onDocumentRename, {
|
||||||
|
getCurrentValue: (doc: Document) => doc?.title ?? '',
|
||||||
|
getEntityId: (doc: Document) => doc?.id ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const folderRename = useInlineRename<Folder>(onFolderRename, {
|
||||||
|
getCurrentValue: (folder: Folder) => folder?.name ?? '',
|
||||||
|
getEntityId: (folder: Folder) => folder?.id ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedDocumentIdsSet,
|
||||||
|
selectedFolderIdsSet,
|
||||||
|
clearSelection,
|
||||||
|
totalSelectionCount,
|
||||||
|
documentRename,
|
||||||
|
folderRename,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocumentViewLogic = ReturnType<typeof useDocumentViewLogic>;
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import React, { type DragEvent } from 'react';
|
||||||
|
import type { DocumentsViewProps } from '../panel/DocumentsPanel';
|
||||||
|
import type { DocumentViewLogic } from './useDocumentViewLogic';
|
||||||
|
|
||||||
|
interface UseFolderItemLogicProps extends DocumentsViewProps {
|
||||||
|
folder: any;
|
||||||
|
viewLogic: DocumentViewLogic;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFolderItemLogic = (props: UseFolderItemLogicProps) => {
|
||||||
|
const {
|
||||||
|
folder,
|
||||||
|
viewLogic,
|
||||||
|
draggedFolderId,
|
||||||
|
onFolderClick,
|
||||||
|
onFolderSelect,
|
||||||
|
onFolderDragOver,
|
||||||
|
onFolderDragLeave,
|
||||||
|
onFolderDrop,
|
||||||
|
onFolderDragStart,
|
||||||
|
onFolderDragEnd,
|
||||||
|
onFolderRename,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedFolderIdsSet,
|
||||||
|
totalSelectionCount,
|
||||||
|
folderRename: {
|
||||||
|
editingId: editingFolderId,
|
||||||
|
draftValue: folderDraft,
|
||||||
|
setDraftValue: setFolderDraft,
|
||||||
|
beginEditing: beginFolderEditing,
|
||||||
|
cancelEditing: cancelFolderEditing,
|
||||||
|
submitEditing: submitFolderEditing,
|
||||||
|
savingId: savingFolderId,
|
||||||
|
attachInputRef: attachFolderInputRef,
|
||||||
|
},
|
||||||
|
} = viewLogic;
|
||||||
|
|
||||||
|
const canDragFolder = folder.id !== 'root';
|
||||||
|
const isDraggingFolder = draggedFolderId === folder.id;
|
||||||
|
const isSelectedFolder = selectedFolderIdsSet?.has(folder.id);
|
||||||
|
const canRenameFolder = Boolean(onFolderRename) && folder.id !== 'root';
|
||||||
|
const isFolderEditing = editingFolderId === folder.id;
|
||||||
|
const folderDraftValue = isFolderEditing ? folderDraft : folder.name;
|
||||||
|
const trimmedFolderDraft = isFolderEditing ? folderDraft.trim() : '';
|
||||||
|
const isFolderSaving = savingFolderId === folder.id;
|
||||||
|
const canSubmitFolder =
|
||||||
|
isFolderEditing && trimmedFolderDraft.length > 0 && trimmedFolderDraft !== folder.name;
|
||||||
|
const allowInlineFolderEdit = canRenameFolder && isSelectedFolder && totalSelectionCount === 1;
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
onClick: (event: React.MouseEvent) => onFolderClick?.(folder, event),
|
||||||
|
onDoubleClick: (event: React.MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
onFolderSelect?.(folder.id);
|
||||||
|
},
|
||||||
|
onDragOver: (event: DragEvent<HTMLElement>) => onFolderDragOver?.(event, folder.id),
|
||||||
|
onDragLeave: onFolderDragLeave,
|
||||||
|
onDrop: (event: DragEvent<HTMLElement>) => onFolderDrop?.(event, folder.id),
|
||||||
|
onDragStart: (event: DragEvent<HTMLElement>) => {
|
||||||
|
if (canDragFolder) {
|
||||||
|
onFolderDragStart?.(event, folder.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragEnd: (event: DragEvent<HTMLElement>) => {
|
||||||
|
if (canDragFolder) {
|
||||||
|
onFolderDragEnd?.(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRenameChange: setFolderDraft,
|
||||||
|
onRenameSubmit: () => submitFolderEditing(folder),
|
||||||
|
onRenameCancel: (event?: React.SyntheticEvent) => cancelFolderEditing(event),
|
||||||
|
onRenameBegin: (event: React.SyntheticEvent) => {
|
||||||
|
if (!allowInlineFolderEdit) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
beginFolderEditing(folder);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
canDragFolder,
|
||||||
|
isDraggingFolder,
|
||||||
|
isSelectedFolder,
|
||||||
|
isFolderEditing,
|
||||||
|
folderDraftValue,
|
||||||
|
isFolderSaving,
|
||||||
|
canSubmitFolder,
|
||||||
|
allowInlineFolderEdit,
|
||||||
|
attachFolderInputRef,
|
||||||
|
handlers,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import DocumentsGrid from '../DocumentsGrid';
|
import { DocumentsList, DocumentsGrid } from '../DocumentsView';
|
||||||
import DocumentsList from '../DocumentsList';
|
|
||||||
import type { DragEvent, ReactNode, RefObject } from 'react';
|
import type { DragEvent, ReactNode, RefObject } from 'react';
|
||||||
import type {
|
import type {
|
||||||
DocumentsListEntry,
|
DocumentsListEntry,
|
||||||
@@ -653,9 +652,9 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
if (!container) return;
|
if (!container) return;
|
||||||
let selector = null;
|
let selector = null;
|
||||||
if (focusedEntryKey.startsWith('document:')) {
|
if (focusedEntryKey.startsWith('document:')) {
|
||||||
selector = `#document-row-${focusedEntryKey.slice('document:'.length)}`;
|
selector = `#document-${focusedEntryKey.slice('document:'.length)}`;
|
||||||
} else if (focusedEntryKey.startsWith('folder:')) {
|
} else if (focusedEntryKey.startsWith('folder:')) {
|
||||||
selector = `#folder-row-${focusedEntryKey.slice('folder:'.length)}`;
|
selector = `#folder-${focusedEntryKey.slice('folder:'.length)}`;
|
||||||
}
|
}
|
||||||
if (!selector) {
|
if (!selector) {
|
||||||
return;
|
return;
|
||||||
@@ -690,10 +689,10 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
const activeDescendantId = useMemo(() => {
|
const activeDescendantId = useMemo(() => {
|
||||||
if (!focusedEntryKey) return undefined;
|
if (!focusedEntryKey) return undefined;
|
||||||
if (focusedEntryKey.startsWith('document:')) {
|
if (focusedEntryKey.startsWith('document:')) {
|
||||||
return `document-row-${focusedEntryKey.slice('document:'.length)}`;
|
return `document-${focusedEntryKey.slice('document:'.length)}`;
|
||||||
}
|
}
|
||||||
if (focusedEntryKey.startsWith('folder:')) {
|
if (focusedEntryKey.startsWith('folder:')) {
|
||||||
return `folder-row-${focusedEntryKey.slice('folder:'.length)}`;
|
return `folder-${focusedEntryKey.slice('folder:'.length)}`;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [focusedEntryKey]);
|
}, [focusedEntryKey]);
|
||||||
@@ -785,7 +784,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
[entries],
|
[entries],
|
||||||
);
|
);
|
||||||
|
|
||||||
const viewProps: DocumentsViewProps = {
|
const viewProps = {
|
||||||
entries,
|
entries,
|
||||||
draggingDocumentIdsSet: draggingSet,
|
draggingDocumentIdsSet: draggingSet,
|
||||||
draggedFolderId,
|
draggedFolderId,
|
||||||
@@ -796,6 +795,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
onFolderDrop,
|
onFolderDrop,
|
||||||
onFolderDragStart,
|
onFolderDragStart,
|
||||||
onFolderDragEnd,
|
onFolderDragEnd,
|
||||||
|
onFolderRename,
|
||||||
onDocumentClick: handleDocumentClick,
|
onDocumentClick: handleDocumentClick,
|
||||||
onDocumentActivate: handleDocumentActivate,
|
onDocumentActivate: handleDocumentActivate,
|
||||||
onDocumentDragStart: handleDocumentDragStartLocal,
|
onDocumentDragStart: handleDocumentDragStartLocal,
|
||||||
@@ -804,22 +804,17 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
onDocumentTagDragLeave: handleDocumentTagDragLeave,
|
onDocumentTagDragLeave: handleDocumentTagDragLeave,
|
||||||
onDocumentTagDrop,
|
onDocumentTagDrop,
|
||||||
onDocumentRename,
|
onDocumentRename,
|
||||||
onFolderRename,
|
|
||||||
ensureAssetUrl,
|
ensureAssetUrl,
|
||||||
getDocumentAsset,
|
getDocumentAsset,
|
||||||
tagLookupById,
|
tagLookupById,
|
||||||
onTagClick: toggleTagFilter,
|
onTagClick: toggleTagFilter,
|
||||||
onCorrespondentClick: toggleCorrespondentFilter,
|
|
||||||
activeCorrespondentIdSet: activeCorrespondentIdSet,
|
|
||||||
activeTagFilters,
|
|
||||||
scrollRef,
|
scrollRef,
|
||||||
// Desk specific
|
activeCorrespondentIdSet: activeCorrespondentIdSet,
|
||||||
onDocumentStackSelect: handleDeskDocumentStackSelect,
|
onCorrespondentClick: toggleCorrespondentFilter,
|
||||||
onPromoteSelection: promoteSelectionOrder,
|
|
||||||
tenantId: currentTenantId,
|
tenantId: currentTenantId,
|
||||||
viewId: deskViewId,
|
viewId: deskViewId,
|
||||||
documentLinks: documentLinkMap,
|
onDocumentStackSelect: handleDeskDocumentStackSelect,
|
||||||
ensureDownloadUrl,
|
onPromoteSelection: promoteSelectionOrder,
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderBody = () => {
|
const renderBody = () => {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const useDocumentDragHandlers = ({
|
|||||||
documentLookup,
|
documentLookup,
|
||||||
setDraggedDocumentIds,
|
setDraggedDocumentIds,
|
||||||
setDraggedFolderId,
|
setDraggedFolderId,
|
||||||
documentsViewMode,
|
documentsViewMode: _documentsViewMode,
|
||||||
}: UseDocumentDragHandlersOptions) => {
|
}: UseDocumentDragHandlersOptions) => {
|
||||||
const dragPreviewRef = useRef<HTMLDivElement | null>(null);
|
const dragPreviewRef = useRef<HTMLDivElement | null>(null);
|
||||||
const normalizedFolderIds = useMemo(
|
const normalizedFolderIds = useMemo(
|
||||||
@@ -100,8 +100,7 @@ const useDocumentDragHandlers = ({
|
|||||||
if (item.type === 'document') {
|
if (item.type === 'document') {
|
||||||
const doc = item.payload;
|
const doc = item.payload;
|
||||||
const rowEl = doc?.id
|
const rowEl = doc?.id
|
||||||
? (document.getElementById(`document-row-${doc.id}`)
|
? document.getElementById(`document-${doc.id}`)
|
||||||
|| document.getElementById(`document-card-${doc.id}`))
|
|
||||||
: null;
|
: null;
|
||||||
const wrapperEl = rowEl instanceof HTMLElement
|
const wrapperEl = rowEl instanceof HTMLElement
|
||||||
? rowEl.querySelector<HTMLElement>('.document-thumbnail-wrapper')
|
? rowEl.querySelector<HTMLElement>('.document-thumbnail-wrapper')
|
||||||
@@ -144,8 +143,7 @@ const useDocumentDragHandlers = ({
|
|||||||
const payload = item.payload;
|
const payload = item.payload;
|
||||||
const folderId = payload as FolderIdentifier;
|
const folderId = payload as FolderIdentifier;
|
||||||
const rowEl = folderId
|
const rowEl = folderId
|
||||||
? (document.getElementById(`folder-row-${folderId}`)
|
? document.getElementById(`folder-${folderId}`)
|
||||||
|| document.getElementById(`folder-card-${folderId}`))
|
|
||||||
: null;
|
: null;
|
||||||
const iconEl = rowEl instanceof HTMLElement
|
const iconEl = rowEl instanceof HTMLElement
|
||||||
? rowEl.querySelector('.thumb-icon, .folder-card__icon')
|
? rowEl.querySelector('.thumb-icon, .folder-card__icon')
|
||||||
@@ -211,16 +209,13 @@ const useDocumentDragHandlers = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isGridView = documentsViewMode === 'grid';
|
|
||||||
const isAlreadySelected = selectedDocumentIds.includes(documentId);
|
const isAlreadySelected = selectedDocumentIds.includes(documentId);
|
||||||
const selection: Identifier[] = isAlreadySelected
|
const selection: Identifier[] = isAlreadySelected
|
||||||
? [...selectedDocumentIds]
|
? [...selectedDocumentIds]
|
||||||
: isGridView
|
: [documentId];
|
||||||
? [...selectedDocumentIds, documentId]
|
|
||||||
: [documentId];
|
|
||||||
const folderSelection: FolderIdentifier[] = [];
|
const folderSelection: FolderIdentifier[] = [];
|
||||||
|
|
||||||
if (!isAlreadySelected && !isGridView) {
|
if (!isAlreadySelected) {
|
||||||
applySelection([documentKey], {
|
applySelection([documentKey], {
|
||||||
anchor: documentKey,
|
anchor: documentKey,
|
||||||
interactedKeys: [documentKey],
|
interactedKeys: [documentKey],
|
||||||
@@ -271,7 +266,6 @@ const useDocumentDragHandlers = ({
|
|||||||
createDragPreview,
|
createDragPreview,
|
||||||
setDraggedFolderId,
|
setDraggedFolderId,
|
||||||
setDraggedDocumentIds,
|
setDraggedDocumentIds,
|
||||||
documentsViewMode,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user