feat: refactor frontend tag interaction handlers

This commit is contained in:
2025-12-09 00:03:55 +01:00
parent c38d88c209
commit 99fa12a1be
11 changed files with 63 additions and 80 deletions
@@ -1,11 +1,11 @@
import React, { useMemo } from 'react';
import DesktopPreviewCard from './DesktopPreviewCard';
import { resolveCorrespondents } from '../../documents/correspondents';
import { getTagColorStyle } from '../../utils/colors';
import type { DocumentId } from '../../types/identifiers';
import type { Document } from '../../types/documents';
import { LayoutCard } from '../logic/LayoutSystem';
import { useCardPointer } from '../interactions/useCardPointer';
import DocumentTags from '../../documents/components/DocumentTags';
import { TagInteractionHandlers } from '../../documents/interactions/useTagInteractions';
const preventAll = (event?: React.SyntheticEvent | Event | null) => {
if (!event) return;
@@ -27,12 +27,7 @@ interface DesktopDocumentCardProps {
onDeselect: (ids: string[]) => void;
selection: string[];
requestCanvasFocus?: () => void;
onTagDragEnter?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
onTagDragOver?: (event: React.DragEvent<HTMLDivElement>, doc: Document) => void;
onTagDragLeave?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
onTagDrop?: (event: React.DragEvent<HTMLDivElement>, doc: Document) => void;
onTagDragStart?: (event: React.DragEvent<HTMLElement>, doc: Document, tag: any) => void;
onTagDragEnd?: (event: React.DragEvent<HTMLElement>) => void;
tagHandlers?: TagInteractionHandlers;
layoutCard: LayoutCard;
}
@@ -50,12 +45,7 @@ const DesktopDocumentCard: React.FC<DesktopDocumentCardProps> = ({
onDeselect,
selection,
requestCanvasFocus,
onTagDragEnter,
onTagDragOver,
onTagDragLeave,
onTagDrop,
onTagDragStart,
onTagDragEnd,
tagHandlers,
layoutCard,
}) => {
const cardPointerHandlers = useCardPointer(
@@ -89,10 +79,10 @@ const DesktopDocumentCard: React.FC<DesktopDocumentCardProps> = ({
aria-hidden={ariaHidden}
ref={(node) => layoutCard?.setRef(node)}
{...cardPointerHandlers}
onDragEnter={(event) => onTagDragEnter?.(event, doc.id!)}
onDragOver={(event) => onTagDragOver?.(event, doc)}
onDragLeave={(event) => onTagDragLeave?.(event, doc.id!)}
onDrop={(event) => onTagDrop?.(event, doc)}
onDragEnter={(event) => tagHandlers?.onTagDragEnter(event, doc.id!)}
onDragOver={(event) => tagHandlers?.onTagDragOver(event, doc)}
onDragLeave={(event) => tagHandlers?.onTagDragLeave(event, doc.id!)}
onDrop={(event) => tagHandlers?.onTagDrop(event, doc)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
preventAll(event);
@@ -123,24 +113,12 @@ const DesktopDocumentCard: React.FC<DesktopDocumentCardProps> = ({
)}
{tags.length > 0 && (
<div className="desk-item__tags" aria-hidden="true">
{tags.map((tag) => {
const colorStyle = getTagColorStyle(tag.color);
const tagClasses = ['badge', 'tag-chip', 'tag-chip--draggable'];
return (
<span
key={tag.id}
className={tagClasses.join(' ')}
style={colorStyle || undefined}
title={tag.label}
draggable
data-desk-tag-chip="true"
onDragStart={(event) => onTagDragStart?.(event, doc, tag)}
onDragEnd={(event) => onTagDragEnd?.(event)}
>
<span className="tag-chip__label">{tag.label}</span>
</span>
);
})}
<DocumentTags
doc={doc}
tags={tags}
tagHandlers={tagHandlers}
onTagClick={tagHandlers?.onTagClick}
/>
</div>
)}
</div>
@@ -9,7 +9,6 @@ import { LayoutStore, LayoutCard } from '../logic/LayoutSystem';
import DesktopDocumentCard from './DesktopDocumentCard';
import usePreviewMetadata from '../hooks/usePreviewMetadata';
import {
TagDragHandlers,
useTagInteractions,
} from '../../documents/interactions/useTagInteractions';
import './workspace-layout.css';
@@ -183,7 +182,7 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
}, []);
// Tag Interactions
const tagDragHandlers: TagDragHandlers = useTagInteractions({
const tagHandlers = useTagInteractions({
onAssignTagToDocument: (docId: string, tagId: string) => {
tags.onAttach?.(docId, tagId);
},
@@ -191,6 +190,7 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
tags.onDetach?.(docId, tagId);
},
requestCanvasFocus: focusShell,
onTagClick: tags.onClick,
});
useEffect(() => {
@@ -392,7 +392,7 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
openDocument(doc, isPreview ? 'preview' : 'sidepanel');
}}
layoutCard={layoutCard}
{...tagDragHandlers}
tagHandlers={tagHandlers}
onSelect={(ids, extend = false) => {
if (!extend) {
handleSelectionChange(ids);
@@ -2,11 +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';
import type { TagInteractionHandlers } from '../interactions/useTagInteractions';
interface DocumentEntryProps {
doc: any;
tagDragHandlers?: TagDragHandlers;
tagHandlers?: TagInteractionHandlers;
viewLogic: DocumentViewLogic;
component: React.ElementType;
className?: string;
@@ -15,8 +15,8 @@ interface DocumentEntryProps {
}
const DocumentEntry: React.FC<DocumentEntryProps> = (props) => {
const { doc, tagDragHandlers, component, className, role, children, viewLogic } = props;
const logic = useDocumentItemLogic({ doc, tagDragHandlers, viewLogic });
const { doc, tagHandlers, component, className, role, children, viewLogic } = props;
const logic = useDocumentItemLogic({ doc, tagHandlers, viewLogic });
return (
<EntryShell
@@ -2,14 +2,14 @@ import React, { useMemo } from 'react';
import { getTagColorStyle } from '../../utils/colors';
import type { Document, DocumentTag } from '../../types/documents';
import type { Identifier } from '../../types/identifiers';
import type { TagDragHandlers } from '../interactions/useTagInteractions';
import type { TagInteractionHandlers } from '../interactions/useTagInteractions';
interface DocumentTagsProps {
tags: DocumentTag[];
tagLookupById?: Map<Identifier, DocumentTag> | null;
onTagClick?: (tagId: Identifier) => void;
doc: Document;
tagDragHandlers?: TagDragHandlers;
tagHandlers?: TagInteractionHandlers;
}
const DocumentTags: React.FC<DocumentTagsProps> = ({
@@ -17,7 +17,7 @@ const DocumentTags: React.FC<DocumentTagsProps> = ({
tagLookupById,
onTagClick,
doc,
tagDragHandlers,
tagHandlers,
}) => {
const sortedTags = useMemo(() => {
return [...tags].sort((a, b) => {
@@ -38,12 +38,13 @@ const DocumentTags: React.FC<DocumentTagsProps> = ({
const style = getTagColorStyle(colorSource);
const tagId = tag?.id ?? null;
const clickable = tagId != null && typeof onTagClick === 'function';
const draggable = !!tagId;
const key = tagId ?? `${doc.id}-tag-${index}`;
return (
<span
key={key}
className="badge tag-chip"
className={`badge tag-chip${draggable ? ' tag-chip--draggable' : ''}`}
style={style || undefined}
title={tag.label}
role={clickable ? 'button' : undefined}
@@ -53,8 +54,8 @@ const DocumentTags: React.FC<DocumentTagsProps> = ({
onTagClick?.(tagId);
} : undefined}
draggable={!!tagId}
onDragStart={(event) => tagId && tagDragHandlers?.onTagDragStart(event, doc, tag)}
onDragEnd={tagDragHandlers?.onTagDragEnd}
onDragStart={(event) => tagId && tagHandlers?.onTagDragStart(event, doc, tag)}
onDragEnd={tagHandlers?.onTagDragEnd}
onKeyDown={clickable ? (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
@@ -12,17 +12,17 @@ import EntryCorrespondents from './EntryCorrespondents';
import EntryTags from './EntryTags';
import FolderEntry from './FolderEntry';
import DocumentEntry from './DocumentEntry';
import { TagDragHandlers } from '../interactions/useTagInteractions';
import { TagInteractionHandlers } from '../interactions/useTagInteractions';
interface DocumentsGridCardProps {
entry: DocumentsListEntry;
viewLogic: DocumentViewLogic;
iconSize?: number;
tagDragHandlers?: TagDragHandlers;
tagHandlers?: TagInteractionHandlers;
}
const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => {
const { entry, iconSize, tagDragHandlers } = props;
const { entry, iconSize, tagHandlers } = props;
const { ensureAssetUrl, getDocumentAsset } = useDocumentsAssetContext();
const { scrollRef, activeCorrespondentIdSet, tagLookupById } = useDocumentsViewStateContext();
const {
@@ -80,7 +80,7 @@ const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => {
return (
<DocumentEntry
doc={doc}
tagDragHandlers={tagDragHandlers}
tagHandlers={tagHandlers}
viewLogic={props.viewLogic}
component="div"
className="document-card document"
@@ -127,7 +127,7 @@ const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => {
tagLookupById={tagLookupById}
onTagClick={onTagClick}
doc={doc}
tagDragHandlers={logic.handlers.tagDragHandlers}
tagHandlers={logic.handlers.tagHandlers}
/>
</div>
</div>
@@ -14,17 +14,17 @@ import EntryTags from './EntryTags';
import FolderEntry from './FolderEntry';
import DocumentEntry from './DocumentEntry';
import { TagDragHandlers } from '../interactions/useTagInteractions';
import { TagInteractionHandlers } from '../interactions/useTagInteractions';
interface DocumentsListRowProps {
entry: DocumentsListEntry;
viewLogic: DocumentViewLogic;
iconSize?: number;
tagDragHandlers?: TagDragHandlers;
tagHandlers?: TagInteractionHandlers;
}
const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
const { entry, iconSize, tagDragHandlers } = props;
const { entry, iconSize, tagHandlers } = props;
const { ensureAssetUrl, getDocumentAsset } = useDocumentsAssetContext();
const { scrollRef, activeCorrespondentIdSet, tagLookupById } = useDocumentsViewStateContext();
const {
@@ -91,7 +91,7 @@ const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
return (
<DocumentEntry
doc={doc}
tagDragHandlers={tagDragHandlers}
tagHandlers={tagHandlers}
viewLogic={props.viewLogic}
component="tr"
className="document"
@@ -142,7 +142,7 @@ const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
tagLookupById={tagLookupById}
onTagClick={onTagClick}
doc={doc}
tagDragHandlers={logic.handlers.tagDragHandlers}
tagHandlers={logic.handlers.tagHandlers}
/>
</div>
</div>
@@ -1,7 +1,7 @@
import React from 'react';
import DocumentTags from './DocumentTags';
import type { Document, DocumentTag } from '../../types/documents';
import type { TagDragHandlers } from '../interactions/useTagInteractions';
import type { TagInteractionHandlers } from '../interactions/useTagInteractions';
import type { Identifier } from '../../types/identifiers';
interface EntryTagsProps {
@@ -9,7 +9,7 @@ interface EntryTagsProps {
tagLookupById?: Map<Identifier, DocumentTag> | null;
onTagClick?: (tagId: Identifier) => void;
doc: Document;
tagDragHandlers?: TagDragHandlers;
tagHandlers?: TagInteractionHandlers;
}
const EntryTags: React.FC<EntryTagsProps> = (props) => {
@@ -23,7 +23,7 @@ const EntryTags: React.FC<EntryTagsProps> = (props) => {
tagLookupById={props.tagLookupById}
onTagClick={props.onTagClick}
doc={props.doc}
tagDragHandlers={props.tagDragHandlers}
tagHandlers={props.tagHandlers}
/>
);
};
@@ -48,6 +48,7 @@ interface UseTagInteractionsArgs {
onAssignTagToDocument?: (docId: Identifier, tagId: Identifier) => void;
onRemoveTagFromDocument?: (docId: Identifier, tagId: Identifier) => void;
requestCanvasFocus?: () => void;
onTagClick?: (tagId: Identifier) => void;
}
interface DraggingTagState {
@@ -55,20 +56,22 @@ interface DraggingTagState {
previewClone?: HTMLElement;
}
export interface TagDragHandlers {
export interface TagInteractionHandlers {
onTagDragEnter: (event: React.DragEvent<HTMLDivElement>, docId: Identifier) => void;
onTagDragOver: (event: React.DragEvent<HTMLDivElement>, doc: Document) => void;
onTagDragLeave: (event: React.DragEvent<HTMLDivElement>, docId: Identifier) => void;
onTagDrop: (event: React.DragEvent<HTMLDivElement>, doc: Document) => void;
onTagDragStart: (event: React.DragEvent<HTMLElement>, doc: Document, tag: DocumentTag) => void;
onTagDragEnd: (event: React.DragEvent<HTMLElement>) => void;
onTagClick?: (tagId: Identifier) => void;
}
export const useTagInteractions = ({
onAssignTagToDocument,
onRemoveTagFromDocument,
requestCanvasFocus,
}: UseTagInteractionsArgs): TagDragHandlers => {
onTagClick,
}: UseTagInteractionsArgs): TagInteractionHandlers => {
const draggingTagRef = useRef<DraggingTagState | null>(null);
const isTagTransfer = useCallback((event: React.DragEvent) => isTagTransferEvent(event), []);
@@ -241,5 +244,6 @@ export const useTagInteractions = ({
onTagDrop,
onTagDragStart,
onTagDragEnd,
onTagClick,
};
};
@@ -5,16 +5,15 @@ import type { Document } from '../../types/documents';
import { useDocumentsCommandContext } from '../context/DocumentsCommandContext';
import { useDocumentsViewStateContext } from '../context/DocumentsViewStateContext';
import type { DocumentViewLogic } from './useDocumentViewLogic';
import { TagDragHandlers } from '../interactions/useTagInteractions';
import { TagInteractionHandlers } from '../interactions/useTagInteractions';
interface UseDocumentItemLogicArgs {
doc: Document;
tagDragHandlers?: TagDragHandlers;
tagHandlers?: TagInteractionHandlers;
viewLogic: DocumentViewLogic; // Retained from original props
}
export const useDocumentItemLogic = ({ doc, tagDragHandlers, viewLogic }: UseDocumentItemLogicArgs) => {
export const useDocumentItemLogic = ({ doc, tagHandlers, viewLogic }: UseDocumentItemLogicArgs) => {
const {
draggingDocumentIdsSet
} = useDocumentsViewStateContext();
@@ -70,10 +69,10 @@ export const useDocumentItemLogic = ({ doc, tagDragHandlers, viewLogic }: UseDoc
},
onDragStart: (event: DragEvent<HTMLElement>) => onDocumentDragStart?.(event, doc),
onDragEnd: (event: DragEvent<HTMLElement>) => onDocumentDragEnd?.(event),
onDragOver: (event: DragEvent<HTMLElement>) => tagDragHandlers?.onTagDragOver(event, doc),
onDragLeave: (event: DragEvent<HTMLElement>) => tagDragHandlers?.onTagDragLeave(event, doc.id),
onDrop: (event: DragEvent<HTMLElement>) => { tagDragHandlers?.onTagDrop(event, doc); },
tagDragHandlers,
onDragOver: (event: DragEvent<HTMLElement>) => tagHandlers?.onTagDragOver(event, doc),
onDragLeave: (event: DragEvent<HTMLElement>) => tagHandlers?.onTagDragLeave(event, doc.id),
onDrop: (event: DragEvent<HTMLElement>) => { tagHandlers?.onTagDrop(event, doc); },
tagHandlers,
onRenameChange: setDocumentDraft,
onRenameSubmit: () => submitDocumentEditing(doc),
onRenameCancel: (event?: React.SyntheticEvent) => cancelDocumentEditing(event),
@@ -48,11 +48,11 @@ interface DocumentsPanelProps extends DocumentsPanelInnerProps {
selectionValue: WorkspaceSelectionValue;
}
import type { TagDragHandlers } from '../interactions/useTagInteractions';
import type { TagInteractionHandlers } from '../interactions/useTagInteractions';
export interface DocumentsViewProps {
entries: DocumentsListEntry[];
tagDragHandlers?: TagDragHandlers;
tagHandlers?: TagInteractionHandlers;
}
const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = (props) => {
@@ -88,7 +88,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = (props) => {
assetContextValue,
viewStateContextValue,
commandContextValue,
tagDragHandlers,
tagHandlers,
scrollRef,
hasDocumentEntries,
} = useDocumentsContextValues(props);
@@ -246,7 +246,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = (props) => {
const viewProps = {
entries,
viewId: currentFolderId,
tagDragHandlers,
tagHandlers,
};
const [iconSizes] = useState({
@@ -38,9 +38,10 @@ export const useDocumentsContextValues = (props: DocumentsPanelInnerProps) => {
} = useDocumentsFilter();
// Handlers
const tagDragHandlers = useTagInteractions({
const tagHandlers = useTagInteractions({
onAssignTagToDocument: props.onDocumentTagAttach,
onRemoveTagFromDocument: props.onDocumentTagDetach,
onTagClick: toggleTagFilter,
});
// Derived State
@@ -147,7 +148,7 @@ export const useDocumentsContextValues = (props: DocumentsPanelInnerProps) => {
handleFolderClick,
handleDocumentDragStartLocal,
handleDocumentDragEndLocal,
tagDragHandlers,
tagHandlers,
toggleTagFilter,
toggleCorrespondentFilter,
});
@@ -158,7 +159,7 @@ export const useDocumentsContextValues = (props: DocumentsPanelInnerProps) => {
handleFolderClick,
handleDocumentDragStartLocal,
handleDocumentDragEndLocal,
tagDragHandlers,
tagHandlers,
toggleTagFilter,
toggleCorrespondentFilter,
};
@@ -198,7 +199,7 @@ export const useDocumentsContextValues = (props: DocumentsPanelInnerProps) => {
assetContextValue,
viewStateContextValue,
commandContextValue,
tagDragHandlers,
tagHandlers,
scrollRef,
hasDocumentEntries,
isSearchLoading,