feat: Implement tag drag-and-drop functionality

This commit is contained in:
2025-12-08 23:49:50 +01:00
parent 757913cf00
commit c38d88c209
9 changed files with 54 additions and 77 deletions
@@ -2,9 +2,11 @@ import React from 'react';
import type { DocumentViewLogic } from '../logic/useDocumentViewLogic';
import { useDocumentItemLogic } from '../logic/useDocumentItemLogic';
import EntryShell from './EntryShell';
import type { TagDragHandlers } from '../interactions/useTagInteractions';
interface DocumentEntryProps {
doc: any;
tagDragHandlers?: TagDragHandlers;
viewLogic: DocumentViewLogic;
component: React.ElementType;
className?: string;
@@ -13,8 +15,8 @@ interface DocumentEntryProps {
}
const DocumentEntry: React.FC<DocumentEntryProps> = (props) => {
const { doc, component, className, role, children, viewLogic } = props;
const logic = useDocumentItemLogic({ doc, viewLogic });
const { doc, tagDragHandlers, component, className, role, children, viewLogic } = props;
const logic = useDocumentItemLogic({ doc, tagDragHandlers, viewLogic });
return (
<EntryShell
@@ -1,24 +1,23 @@
import React, { useMemo } from 'react';
import { getTagColorStyle } from '../../utils/colors';
import type { DocumentTag } from '../../types/documents';
import type { Document, DocumentTag } from '../../types/documents';
import type { Identifier } from '../../types/identifiers';
import type { TagDragHandlers } from '../interactions/useTagInteractions';
interface DocumentTagsProps {
tags: DocumentTag[];
tagLookupById?: Map<Identifier, DocumentTag> | null;
onTagClick?: (tagId: Identifier) => void;
docId: Identifier;
onTagDragStart?: (event: React.DragEvent<HTMLElement>, tag: DocumentTag) => void;
onTagDragEnd?: (event: React.DragEvent<HTMLElement>) => void;
doc: Document;
tagDragHandlers?: TagDragHandlers;
}
const DocumentTags: React.FC<DocumentTagsProps> = ({
tags,
tagLookupById,
onTagClick,
docId,
onTagDragStart,
onTagDragEnd,
doc,
tagDragHandlers,
}) => {
const sortedTags = useMemo(() => {
return [...tags].sort((a, b) => {
@@ -39,7 +38,7 @@ const DocumentTags: React.FC<DocumentTagsProps> = ({
const style = getTagColorStyle(colorSource);
const tagId = tag?.id ?? null;
const clickable = tagId != null && typeof onTagClick === 'function';
const key = tagId ?? `${docId}-tag-${index}`;
const key = tagId ?? `${doc.id}-tag-${index}`;
return (
<span
@@ -53,17 +52,9 @@ const DocumentTags: React.FC<DocumentTagsProps> = ({
if (tagId == null) return;
onTagClick?.(tagId);
} : undefined}
draggable
onDragStart={(event) => {
// Let the hook handle the data transfer and UI
if (tagId && onTagDragStart) {
onTagDragStart(event, tag);
}
}}
onDragEnd={(event) => {
// Let the hook handle the cleanup and logic
onTagDragEnd?.(event);
}}
draggable={!!tagId}
onDragStart={(event) => tagId && tagDragHandlers?.onTagDragStart(event, doc, tag)}
onDragEnd={tagDragHandlers?.onTagDragEnd}
onKeyDown={clickable ? (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
@@ -12,20 +12,22 @@ import EntryCorrespondents from './EntryCorrespondents';
import EntryTags from './EntryTags';
import FolderEntry from './FolderEntry';
import DocumentEntry from './DocumentEntry';
import { TagDragHandlers } from '../interactions/useTagInteractions';
interface DocumentsGridCardProps {
entry: DocumentsListEntry;
viewLogic: DocumentViewLogic;
iconSize?: number;
tagDragHandlers?: TagDragHandlers;
}
const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => {
const { entry, iconSize } = props;
const { entry, iconSize, tagDragHandlers } = props;
const { ensureAssetUrl, getDocumentAsset } = useDocumentsAssetContext();
const { scrollRef, activeCorrespondentIdSet, tagLookupById } = useDocumentsViewStateContext();
const {
correspondents: { onClick: onCorrespondentClick },
tags: { onClick: onTagClick, onDetach: onDocumentTagDetach }
tags: { onClick: onTagClick }
} = useDocumentsCommandContext();
if (entry.type === 'folder') {
@@ -78,6 +80,7 @@ const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => {
return (
<DocumentEntry
doc={doc}
tagDragHandlers={tagDragHandlers}
viewLogic={props.viewLogic}
component="div"
className="document-card document"
@@ -123,10 +126,8 @@ const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => {
tags={doc.tags || []}
tagLookupById={tagLookupById}
onTagClick={onTagClick}
docId={doc.id}
onDocumentTagDetach={onDocumentTagDetach}
onTagDragStart={logic.handlers.onTagDragStart}
onTagDragEnd={logic.handlers.onTagDragEnd}
doc={doc}
tagDragHandlers={logic.handlers.tagDragHandlers}
/>
</div>
</div>
@@ -14,19 +14,22 @@ import EntryTags from './EntryTags';
import FolderEntry from './FolderEntry';
import DocumentEntry from './DocumentEntry';
import { TagDragHandlers } from '../interactions/useTagInteractions';
interface DocumentsListRowProps {
entry: DocumentsListEntry;
viewLogic: DocumentViewLogic;
iconSize?: number;
tagDragHandlers?: TagDragHandlers;
}
const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
const { entry, iconSize } = props;
const { entry, iconSize, tagDragHandlers } = props;
const { ensureAssetUrl, getDocumentAsset } = useDocumentsAssetContext();
const { scrollRef, activeCorrespondentIdSet, tagLookupById } = useDocumentsViewStateContext();
const {
correspondents: { onClick: onCorrespondentClick },
tags: { onClick: onTagClick, onDetach: onDocumentTagDetach }
tags: { onClick: onTagClick }
} = useDocumentsCommandContext();
if (entry.type === 'folder') {
@@ -88,6 +91,7 @@ const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
return (
<DocumentEntry
doc={doc}
tagDragHandlers={tagDragHandlers}
viewLogic={props.viewLogic}
component="tr"
className="document"
@@ -137,10 +141,8 @@ const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
tags={doc.tags || []}
tagLookupById={tagLookupById}
onTagClick={onTagClick}
docId={doc.id}
onDocumentTagDetach={onDocumentTagDetach}
onTagDragStart={logic.handlers.onTagDragStart}
onTagDragEnd={logic.handlers.onTagDragEnd}
doc={doc}
tagDragHandlers={logic.handlers.tagDragHandlers}
/>
</div>
</div>
@@ -1,16 +1,15 @@
import React from 'react';
import DocumentTags from './DocumentTags';
import type { Document, DocumentTag } from '../../types/documents';
import type { TagDragHandlers } from '../interactions/useTagInteractions';
import type { Identifier } from '../../types/identifiers';
import type { DocumentTag } from '../../types/documents';
interface EntryTagsProps {
tags: DocumentTag[];
tagLookupById?: Map<Identifier, DocumentTag> | null;
onTagClick?: (tagId: Identifier) => void;
docId: Identifier;
onDocumentTagDetach?: (docId: Identifier, tagId: Identifier) => void;
onTagDragStart?: (event: React.DragEvent<HTMLElement>, tag: DocumentTag) => void;
onTagDragEnd?: (event: React.DragEvent<HTMLElement>) => void;
doc: Document;
tagDragHandlers?: TagDragHandlers;
}
const EntryTags: React.FC<EntryTagsProps> = (props) => {
@@ -23,10 +22,8 @@ const EntryTags: React.FC<EntryTagsProps> = (props) => {
tags={props.tags}
tagLookupById={props.tagLookupById}
onTagClick={props.onTagClick}
docId={props.docId}
onDocumentTagDetach={props.onDocumentTagDetach}
onTagDragStart={props.onTagDragStart}
onTagDragEnd={props.onTagDragEnd}
doc={props.doc}
tagDragHandlers={props.tagDragHandlers}
/>
);
};