refactor(documents): encapsulate panel logic and state into dedicated React contexts

This commit is contained in:
2025-12-08 12:31:30 +01:00
parent 44cf4603fe
commit 73468bbb02
13 changed files with 524 additions and 338 deletions
+14 -3
View File
@@ -8,6 +8,8 @@ import DocumentsGridCard from './components/DocumentsGridCard';
import DocumentsListContainer from './components/DocumentsListContainer'; import DocumentsListContainer from './components/DocumentsListContainer';
import DocumentsGridContainer from './components/DocumentsGridContainer'; import DocumentsGridContainer from './components/DocumentsGridContainer';
import type { DocumentsViewProps } from './panel/DocumentsPanel'; import type { DocumentsViewProps } from './panel/DocumentsPanel';
import { useDocumentsViewStateContext } from './context/DocumentsViewStateContext';
import { useDocumentsCommandContext } from './context/DocumentsCommandContext';
interface AbstractDocumentsViewProps<CProps extends { clearSelection: () => void; children: React.ReactNode }> extends DocumentsViewProps { interface AbstractDocumentsViewProps<CProps extends { clearSelection: () => void; children: React.ReactNode }> extends DocumentsViewProps {
ContainerComponent: React.ComponentType<CProps>; ContainerComponent: React.ComponentType<CProps>;
@@ -22,7 +24,16 @@ const AbstractDocumentsView = <CProps extends { clearSelection: () => void; chil
containerProps, containerProps,
...props ...props
}: AbstractDocumentsViewProps<CProps>) => { }: AbstractDocumentsViewProps<CProps>) => {
const { entries, onDocumentRename, onFolderRename, onFolderSelect, scrollRef, viewId } = props; const { entries, viewMode } = props;
const {
viewId,
scrollRef
} = useDocumentsViewStateContext();
const {
document: { onRename: onDocumentRename },
folder: { onRename: onFolderRename, onSelect: onFolderSelect }
} = useDocumentsCommandContext();
const viewLogic = useDocumentViewLogic({ const viewLogic = useDocumentViewLogic({
onDocumentRename, onDocumentRename,
onFolderRename, onFolderRename,
@@ -30,8 +41,8 @@ const AbstractDocumentsView = <CProps extends { clearSelection: () => void; chil
const { handleKeyDown, handleFocus } = useDocumentsNavigation({ const { handleKeyDown, handleFocus } = useDocumentsNavigation({
entries, entries,
onFolderSelect, onFolderSelect,
viewMode: props.viewMode, viewMode: viewMode || props.viewMode,
scrollRef: props.scrollRef, scrollRef: scrollRef,
}); });
const { clearSelection } = viewLogic; const { clearSelection } = viewLogic;
@@ -1,10 +1,9 @@
import React from 'react'; import React from 'react';
import type { DocumentsViewProps } from '../panel/DocumentsPanel';
import type { DocumentViewLogic } from '../logic/useDocumentViewLogic'; import type { DocumentViewLogic } from '../logic/useDocumentViewLogic';
import { useDocumentItemLogic } from '../logic/useDocumentItemLogic'; import { useDocumentItemLogic } from '../logic/useDocumentItemLogic';
import EntryShell from './EntryShell'; import EntryShell from './EntryShell';
interface DocumentEntryProps extends DocumentsViewProps { interface DocumentEntryProps {
doc: any; doc: any;
viewLogic: DocumentViewLogic; viewLogic: DocumentViewLogic;
component: React.ElementType; component: React.ElementType;
@@ -15,7 +14,7 @@ interface DocumentEntryProps extends DocumentsViewProps {
const DocumentEntry: React.FC<DocumentEntryProps> = (props) => { const DocumentEntry: React.FC<DocumentEntryProps> = (props) => {
const { doc, component, className, role, children, viewLogic } = props; const { doc, component, className, role, children, viewLogic } = props;
const logic = useDocumentItemLogic({ doc, viewLogic, ...props }); const logic = useDocumentItemLogic({ doc, viewLogic });
return ( return (
<EntryShell <EntryShell
@@ -3,7 +3,9 @@ import { FolderIcon } from '../../components/icons';
import DocumentThumbnailImage from '../DocumentThumbnailImage'; import DocumentThumbnailImage from '../DocumentThumbnailImage';
import { resolveCorrespondents } from '../correspondents'; import { resolveCorrespondents } from '../correspondents';
import type { DocumentsListEntry } from '../../types/documents'; import type { DocumentsListEntry } from '../../types/documents';
import type { DocumentsViewProps } from '../panel/DocumentsPanel'; import { useDocumentsAssetContext } from '../context/DocumentsAssetContext';
import { useDocumentsViewStateContext } from '../context/DocumentsViewStateContext';
import { useDocumentsCommandContext } from '../context/DocumentsCommandContext';
import type { DocumentViewLogic } from '../logic/useDocumentViewLogic'; import type { DocumentViewLogic } from '../logic/useDocumentViewLogic';
import EditableEntryTitle from './EditableEntryTitle'; import EditableEntryTitle from './EditableEntryTitle';
import EntryCorrespondents from './EntryCorrespondents'; import EntryCorrespondents from './EntryCorrespondents';
@@ -11,7 +13,7 @@ import EntryTags from './EntryTags';
import FolderEntry from './FolderEntry'; import FolderEntry from './FolderEntry';
import DocumentEntry from './DocumentEntry'; import DocumentEntry from './DocumentEntry';
interface DocumentsGridCardProps extends DocumentsViewProps { interface DocumentsGridCardProps {
entry: DocumentsListEntry; entry: DocumentsListEntry;
viewLogic: DocumentViewLogic; viewLogic: DocumentViewLogic;
iconSize?: number; iconSize?: number;
@@ -19,6 +21,12 @@ interface DocumentsGridCardProps extends DocumentsViewProps {
const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => { const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => {
const { entry, iconSize } = props; const { entry, iconSize } = props;
const { ensureAssetUrl, getDocumentAsset } = useDocumentsAssetContext();
const { scrollRef, activeCorrespondentIdSet, tagLookupById } = useDocumentsViewStateContext();
const {
correspondents: { onClick: onCorrespondentClick },
tags: { onClick: onTagClick, onDetach: onDocumentTagDetach }
} = useDocumentsCommandContext();
if (entry.type === 'folder') { if (entry.type === 'folder') {
const folder = entry.folder; const folder = entry.folder;
@@ -26,8 +34,8 @@ const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => {
return ( return (
<FolderEntry <FolderEntry
{...props}
folder={folder} folder={folder}
viewLogic={props.viewLogic}
component="div" component="div"
className="document-card folder-card" className="document-card folder-card"
role="listitem" role="listitem"
@@ -69,8 +77,8 @@ const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => {
return ( return (
<DocumentEntry <DocumentEntry
{...props}
doc={doc} doc={doc}
viewLogic={props.viewLogic}
component="div" component="div"
className="document-card document" className="document-card document"
role="listitem" role="listitem"
@@ -79,18 +87,18 @@ const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => {
<> <>
<DocumentThumbnailImage <DocumentThumbnailImage
document={doc} document={doc}
ensureAssetUrl={props.ensureAssetUrl} ensureAssetUrl={ensureAssetUrl}
getAsset={props.getDocumentAsset} getAsset={getDocumentAsset}
alt={`Thumbnail for ${doc.title}`} alt={`Thumbnail for ${doc.title}`}
maxSize={iconSize} maxSize={iconSize}
scrollRootRef={props.scrollRef} scrollRootRef={scrollRef}
/> />
<div className="document-card__meta"> <div className="document-card__meta">
<div className="document-card__title" title={doc.title}> <div className="document-card__title" title={doc.title}>
<EntryCorrespondents <EntryCorrespondents
correspondents={correspondents} correspondents={correspondents}
activeCorrespondentIdSet={props.activeCorrespondentIdSet} activeCorrespondentIdSet={activeCorrespondentIdSet}
onCorrespondentClick={props.onCorrespondentClick} onCorrespondentClick={onCorrespondentClick}
/> />
<div className="document-card__title-row"> <div className="document-card__title-row">
<EditableEntryTitle <EditableEntryTitle
@@ -113,10 +121,10 @@ const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => {
<div className="document-card__tags"> <div className="document-card__tags">
<EntryTags <EntryTags
tags={doc.tags || []} tags={doc.tags || []}
tagLookupById={props.tagLookupById} tagLookupById={tagLookupById}
onTagClick={props.onTagClick} onTagClick={onTagClick}
docId={doc.id} docId={doc.id}
onDocumentTagDetach={props.onDocumentTagDetach} onDocumentTagDetach={onDocumentTagDetach}
onTagDragStart={logic.handlers.onTagDragStart} onTagDragStart={logic.handlers.onTagDragStart}
onTagDragEnd={logic.handlers.onTagDragEnd} onTagDragEnd={logic.handlers.onTagDragEnd}
/> />
@@ -128,4 +136,5 @@ const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => {
); );
}; };
export default DocumentsGridCard; export default DocumentsGridCard;
@@ -4,7 +4,9 @@ import { formatDate } from '../../utils/date';
import DocumentThumbnailImage from '../DocumentThumbnailImage'; import DocumentThumbnailImage from '../DocumentThumbnailImage';
import { resolveCorrespondents } from '../correspondents'; import { resolveCorrespondents } from '../correspondents';
import type { DocumentsListEntry } from '../../types/documents'; import type { DocumentsListEntry } from '../../types/documents';
import type { DocumentsViewProps } from '../panel/DocumentsPanel'; import { useDocumentsAssetContext } from '../context/DocumentsAssetContext';
import { useDocumentsViewStateContext } from '../context/DocumentsViewStateContext';
import { useDocumentsCommandContext } from '../context/DocumentsCommandContext';
import type { DocumentViewLogic } from '../logic/useDocumentViewLogic'; import type { DocumentViewLogic } from '../logic/useDocumentViewLogic';
import EditableEntryTitle from './EditableEntryTitle'; import EditableEntryTitle from './EditableEntryTitle';
import EntryCorrespondents from './EntryCorrespondents'; import EntryCorrespondents from './EntryCorrespondents';
@@ -12,7 +14,7 @@ import EntryTags from './EntryTags';
import FolderEntry from './FolderEntry'; import FolderEntry from './FolderEntry';
import DocumentEntry from './DocumentEntry'; import DocumentEntry from './DocumentEntry';
interface DocumentsListRowProps extends DocumentsViewProps { interface DocumentsListRowProps {
entry: DocumentsListEntry; entry: DocumentsListEntry;
viewLogic: DocumentViewLogic; viewLogic: DocumentViewLogic;
iconSize?: number; iconSize?: number;
@@ -20,6 +22,12 @@ interface DocumentsListRowProps extends DocumentsViewProps {
const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => { const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
const { entry, iconSize } = props; const { entry, iconSize } = props;
const { ensureAssetUrl, getDocumentAsset } = useDocumentsAssetContext();
const { scrollRef, activeCorrespondentIdSet, tagLookupById } = useDocumentsViewStateContext();
const {
correspondents: { onClick: onCorrespondentClick },
tags: { onClick: onTagClick, onDetach: onDocumentTagDetach }
} = useDocumentsCommandContext();
if (entry.type === 'folder') { if (entry.type === 'folder') {
const folder = entry.folder; const folder = entry.folder;
@@ -27,8 +35,8 @@ const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
return ( return (
<FolderEntry <FolderEntry
{...props}
folder={folder} folder={folder}
viewLogic={props.viewLogic}
component="tr" component="tr"
className="folder" className="folder"
> >
@@ -79,8 +87,8 @@ const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
return ( return (
<DocumentEntry <DocumentEntry
{...props}
doc={doc} doc={doc}
viewLogic={props.viewLogic}
component="tr" component="tr"
className="document" className="document"
> >
@@ -89,10 +97,10 @@ const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
<td className="thumb-cell"> <td className="thumb-cell">
<DocumentThumbnailImage <DocumentThumbnailImage
document={doc} document={doc}
ensureAssetUrl={props.ensureAssetUrl} ensureAssetUrl={ensureAssetUrl}
getAsset={props.getDocumentAsset} getAsset={getDocumentAsset}
alt={`Thumbnail for ${doc.title}`} alt={`Thumbnail for ${doc.title}`}
scrollRootRef={props.scrollRef} scrollRootRef={scrollRef}
maxSize={props.iconSize} maxSize={props.iconSize}
/> />
</td> </td>
@@ -102,8 +110,8 @@ const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
<span className="doc-name__title"> <span className="doc-name__title">
<EntryCorrespondents <EntryCorrespondents
correspondents={correspondents} correspondents={correspondents}
activeCorrespondentIdSet={props.activeCorrespondentIdSet} activeCorrespondentIdSet={activeCorrespondentIdSet}
onCorrespondentClick={props.onCorrespondentClick} onCorrespondentClick={onCorrespondentClick}
/> />
<span className="doc-name__primary"> <span className="doc-name__primary">
<EditableEntryTitle <EditableEntryTitle
@@ -127,10 +135,10 @@ const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
<div className="doc-name__tags"> <div className="doc-name__tags">
<EntryTags <EntryTags
tags={doc.tags || []} tags={doc.tags || []}
tagLookupById={props.tagLookupById} tagLookupById={tagLookupById}
onTagClick={props.onTagClick} onTagClick={onTagClick}
docId={doc.id} docId={doc.id}
onDocumentTagDetach={props.onDocumentTagDetach} onDocumentTagDetach={onDocumentTagDetach}
onTagDragStart={logic.handlers.onTagDragStart} onTagDragStart={logic.handlers.onTagDragStart}
onTagDragEnd={logic.handlers.onTagDragEnd} onTagDragEnd={logic.handlers.onTagDragEnd}
/> />
@@ -145,4 +153,5 @@ const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
); );
}; };
export default DocumentsListRow; export default DocumentsListRow;
@@ -1,10 +1,9 @@
import React from 'react'; import React from 'react';
import type { DocumentsViewProps } from '../panel/DocumentsPanel';
import type { DocumentViewLogic } from '../logic/useDocumentViewLogic'; import type { DocumentViewLogic } from '../logic/useDocumentViewLogic';
import { useFolderItemLogic } from '../features/folders/useFolderItemLogic'; import { useFolderItemLogic } from '../features/folders/useFolderItemLogic';
import EntryShell from './EntryShell'; import EntryShell from './EntryShell';
interface FolderEntryProps extends DocumentsViewProps { interface FolderEntryProps {
folder: any; folder: any;
viewLogic: DocumentViewLogic; viewLogic: DocumentViewLogic;
component: React.ElementType; component: React.ElementType;
@@ -15,7 +14,7 @@ interface FolderEntryProps extends DocumentsViewProps {
const FolderEntry: React.FC<FolderEntryProps> = (props) => { const FolderEntry: React.FC<FolderEntryProps> = (props) => {
const { folder, component, className, role, children, viewLogic } = props; const { folder, component, className, role, children, viewLogic } = props;
const logic = useFolderItemLogic({ folder, viewLogic, ...props }); const logic = useFolderItemLogic({ folder, viewLogic });
return ( return (
<EntryShell <EntryShell
@@ -0,0 +1,10 @@
import { createContext, useContext } from 'react';
interface DocumentsAssetContextValue {
ensureAssetUrl?: (...args: any[]) => unknown;
getDocumentAsset?: (...args: any[]) => unknown;
}
export const DocumentsAssetContext = createContext<DocumentsAssetContextValue>({});
export const useDocumentsAssetContext = () => useContext(DocumentsAssetContext);
@@ -0,0 +1,50 @@
import React, { createContext, useContext, type DragEvent } from 'react';
import type { Document } from '../../types/documents';
import type { Identifier } from '../../types/identifiers';
interface DocumentsCommandContextValue {
folder: {
onClick?: (folder: any, event: React.MouseEvent) => void;
onSelect?: (folderId: Identifier | 'root') => void;
onRename?: (folderId: Identifier | 'root', nextName: string) => Promise<boolean> | boolean;
onDrag: {
start?: (event: DragEvent<HTMLElement>, folderId: Identifier | 'root') => void;
end?: (event: DragEvent<HTMLElement>) => void;
over?: (event: DragEvent<HTMLElement>, folderId: Identifier | 'root') => void;
leave?: (event: DragEvent<HTMLElement>) => void;
drop?: (event: DragEvent<HTMLElement>, folderId: Identifier | 'root') => void;
};
};
document: {
onRename?: (documentId: Identifier, nextTitle: string) => Promise<boolean> | boolean;
onDrag: {
start?: (event: DragEvent<HTMLElement>, document: Document) => void;
end?: (event: DragEvent<HTMLElement>) => void;
};
};
tags: {
onDrag: {
start?: (event: DragEvent<HTMLElement>, docId: Identifier, tagId: Identifier) => void;
end?: (event: DragEvent<HTMLElement>) => void;
over?: (event: DragEvent<HTMLElement>, docId: Identifier) => void;
leave?: (event: DragEvent<HTMLElement>) => void;
};
onAttach?: (documentId: Identifier, tagId: Identifier) => void;
onDetach?: (documentId: Identifier, tagId: Identifier) => void;
onClick?: (tagId: Identifier) => void;
};
correspondents: {
onClick?: (correspondentId: Identifier) => void;
};
// General entry pointer for selection/etc
onEntryPointer?: (entry: any, event: any) => void;
}
export const DocumentsCommandContext = createContext<DocumentsCommandContextValue>({
folder: { onDrag: {} },
document: { onDrag: {} },
tags: { onDrag: {} },
correspondents: {},
});
export const useDocumentsCommandContext = () => useContext(DocumentsCommandContext);
@@ -0,0 +1,16 @@
import { createContext, useContext, type RefObject } from 'react';
import type { DocumentTag } from '../../types/documents';
import type { Identifier } from '../../types/identifiers';
interface DocumentsViewStateContextValue {
viewId?: string | null;
scrollRef?: RefObject<HTMLElement | null>;
tagLookupById?: Map<Identifier, DocumentTag> | null;
activeCorrespondentIdSet?: Set<Identifier> | null;
draggingDocumentIdsSet?: Set<Identifier> | null;
draggedFolderId?: Identifier | 'root' | null;
}
export const DocumentsViewStateContext = createContext<DocumentsViewStateContextValue>({});
export const useDocumentsViewStateContext = () => useContext(DocumentsViewStateContext);
@@ -1,26 +1,34 @@
import React, { type DragEvent } from 'react'; import React, { type DragEvent } from 'react';
import type { DocumentsViewProps } from '../../panel/DocumentsPanel';
import type { DocumentViewLogic } from '../../logic/useDocumentViewLogic'; import type { DocumentViewLogic } from '../../logic/useDocumentViewLogic';
import { useDocumentsCommandContext } from '../../context/DocumentsCommandContext';
import { useDocumentsViewStateContext } from '../../context/DocumentsViewStateContext';
interface UseFolderItemLogicProps extends DocumentsViewProps { interface UseFolderItemLogicProps {
folder: any; folder: any;
viewLogic: DocumentViewLogic; viewLogic: DocumentViewLogic;
} }
export const useFolderItemLogic = (props: UseFolderItemLogicProps) => { export const useFolderItemLogic = (props: UseFolderItemLogicProps) => {
const { folder, viewLogic } = props;
const { const {
folder,
viewLogic,
draggedFolderId, draggedFolderId,
onFolderClick, } = useDocumentsViewStateContext();
onFolderSelect,
onFolderDragOver, const {
onFolderDragLeave, folder: {
onFolderDrop, onClick: onFolderClick,
onFolderDragStart, onSelect: onFolderSelect,
onFolderDragEnd, onRename: onFolderRename,
onFolderRename, onDrag: {
} = props; start: onFolderDragStart,
end: onFolderDragEnd,
over: onFolderDragOver,
leave: onFolderDragLeave,
drop: onFolderDrop,
}
}
} = useDocumentsCommandContext();
const { const {
selectedFolderIdsSet, selectedFolderIdsSet,
@@ -92,3 +100,5 @@ export const useFolderItemLogic = (props: UseFolderItemLogicProps) => {
handlers, handlers,
}; };
}; };
@@ -3,30 +3,38 @@ import { parseTagTransferPayload } from '../features/tagging/tagTransfer';
import { createDocumentEntryKey } from '../../app/entryKey'; import { createDocumentEntryKey } from '../../app/entryKey';
import { useDocumentOpen } from '../../lib/context/DocumentOpenContext'; import { useDocumentOpen } from '../../lib/context/DocumentOpenContext';
import type { Document } from '../../types/documents'; import type { Document } from '../../types/documents';
import type { DocumentsViewProps } from '../panel/DocumentsPanel'; import { useDocumentsCommandContext } from '../context/DocumentsCommandContext';
import { useDocumentsViewStateContext } from '../context/DocumentsViewStateContext';
import type { DocumentViewLogic } from './useDocumentViewLogic'; import type { DocumentViewLogic } from './useDocumentViewLogic';
import type { Identifier } from '../../types/identifiers'; import type { Identifier } from '../../types/identifiers';
interface UseDocumentItemLogicProps extends DocumentsViewProps { interface UseDocumentItemLogicProps {
doc: Document; doc: Document;
viewLogic: DocumentViewLogic; viewLogic: DocumentViewLogic;
onEntryPointer?: (entry: any, event: any) => void;
} }
export const useDocumentItemLogic = (props: UseDocumentItemLogicProps) => { export const useDocumentItemLogic = (props: UseDocumentItemLogicProps) => {
const { doc, viewLogic } = props;
const { const {
doc, draggingDocumentIdsSet
viewLogic, } = useDocumentsViewStateContext();
draggingDocumentIdsSet,
onDocumentDragStart, const {
onDocumentDragEnd, document: {
onDocumentTagDragStart, onDrag: { start: onDocumentDragStart, end: onDocumentDragEnd },
onDocumentTagDragEnd, onRename: onDocumentRename
onDocumentTagDragOver, },
onDocumentTagDragLeave, tags: {
onDocumentTagAttach, onDrag: {
onDocumentRename, start: onDocumentTagDragStart,
} = props; end: onDocumentTagDragEnd,
over: onDocumentTagDragOver,
leave: onDocumentTagDragLeave,
},
onAttach: onDocumentTagAttach
},
onEntryPointer
} = useDocumentsCommandContext();
const { const {
selectedDocumentIdsSet, selectedDocumentIdsSet,
@@ -58,8 +66,8 @@ export const useDocumentItemLogic = (props: UseDocumentItemLogicProps) => {
const handlers = { const handlers = {
onClick: (event: React.MouseEvent) => { onClick: (event: React.MouseEvent) => {
if (props.onEntryPointer) { if (onEntryPointer) {
props.onEntryPointer({ type: 'document', id: doc.id, key: createDocumentEntryKey(doc.id), document: doc }, event); onEntryPointer({ type: 'document', id: doc.id, key: createDocumentEntryKey(doc.id), document: doc }, event);
} else { } else {
const key = createDocumentEntryKey(doc.id); const key = createDocumentEntryKey(doc.id);
handleEntrySelection(key, event); handleEntrySelection(key, event);
@@ -106,3 +114,5 @@ export const useDocumentItemLogic = (props: UseDocumentItemLogicProps) => {
handlers, handlers,
}; };
}; };
+78 -270
View File
@@ -6,21 +6,16 @@ import React, {
useEffect, useEffect,
} from 'react'; } from 'react';
import { DocumentsList, DocumentsGrid } from '../DocumentsView'; import { DocumentsList, DocumentsGrid } from '../DocumentsView';
import type { DragEvent, ReactNode, RefObject } from 'react'; import type { ReactNode } from 'react';
import type { import type {
DocumentsListEntry, DocumentsListEntry,
FolderEventHandler,
Document,
DocumentTag,
} from '../../types/documents'; } from '../../types/documents';
import DesktopWorkspace from '../../desktop/components/DesktopWorkspace'; import DesktopWorkspace from '../../desktop/components/DesktopWorkspace';
import { isTagTransferEvent } from '../features/tagging/tagTransfer';
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../features/selection/useEntryPointer';
import { import {
WorkspaceSelectionProvider, WorkspaceSelectionProvider,
useWorkspaceSelectionContext, useWorkspaceSelectionContext,
type WorkspaceSelectionValue,
} from '../../app/WorkspaceSelectionContext'; } from '../../app/WorkspaceSelectionContext';
import type { WorkspaceSelectionValue } from '../../app/WorkspaceSelectionContext';
import DocumentsPanelHeader, { import DocumentsPanelHeader, {
DocumentsPanelHeaderConfig, DocumentsPanelHeaderConfig,
DocumentsHeaderBreadcrumb, DocumentsHeaderBreadcrumb,
@@ -33,14 +28,17 @@ import {
DEFAULT_LIST_ICON_SIZE, DEFAULT_LIST_ICON_SIZE,
DEFAULT_DESKTOP_CARD_SIZE, DEFAULT_DESKTOP_CARD_SIZE,
} from '../../constants/documents'; } from '../../constants/documents';
import type { Identifier } from '../../types/identifiers'; import { DocumentsAssetContext } from '../context/DocumentsAssetContext';
import { DocumentsViewStateContext } from '../context/DocumentsViewStateContext';
import { DocumentsCommandContext } from '../context/DocumentsCommandContext';
import { useDocumentsContextValues } from './useDocumentsContextValues';
const EntryType = { const EntryType = {
folder: 'folder', folder: 'folder' as const,
document: 'document', document: 'document' as const,
}; };
interface DocumentsPanelInnerProps { export interface DocumentsPanelInnerProps {
headerLeading?: ReactNode; headerLeading?: ReactNode;
onBreadcrumbNavigate?: (crumb: DocumentsHeaderBreadcrumb) => void; onBreadcrumbNavigate?: (crumb: DocumentsHeaderBreadcrumb) => void;
[key: string]: any; [key: string]: any;
@@ -52,105 +50,60 @@ interface DocumentsPanelProps extends DocumentsPanelInnerProps {
export interface DocumentsViewProps { export interface DocumentsViewProps {
entries: DocumentsListEntry[]; entries: DocumentsListEntry[];
draggingDocumentIdsSet?: Set<Identifier> | null;
draggedFolderId?: Identifier | 'root' | null;
ensureAssetUrl?: (...args: any[]) => unknown;
getDocumentAsset?: (...args: any[]) => unknown;
onFolderClick?: FolderEventHandler;
onFolderSelect?: (folderId: Identifier | 'root') => void;
onFolderDragOver?: (event: DragEvent<HTMLElement>, folderId: Identifier | 'root') => void;
onFolderDragLeave?: (event: DragEvent<HTMLElement>) => void;
onFolderDrop?: (event: DragEvent<HTMLElement>, folderId: Identifier | 'root') => void;
onFolderDragStart?: (event: DragEvent<HTMLElement>, folderId: Identifier | 'root') => void;
onFolderDragEnd?: (event: DragEvent<HTMLElement>) => void;
onFolderRename?: (folderId: Identifier | 'root', nextName: string) => Promise<boolean> | boolean;
onDocumentDragStart?: (event: DragEvent<HTMLElement>, document: Document) => void;
onDocumentDragEnd?: (event: DragEvent<HTMLElement>) => void;
onDocumentTagDragStart?: (event: DragEvent<HTMLElement>, docId: Identifier, tagId: Identifier) => void;
onDocumentTagDragEnd?: (event: DragEvent<HTMLElement>) => void;
onDocumentTagDragOver?: (event: DragEvent<HTMLElement>, docId: Identifier) => void;
onDocumentTagDragLeave?: (event: DragEvent<HTMLElement>) => void;
onDocumentTagAttach?: (documentId: Identifier, tagId: Identifier) => void;
onDocumentTagDetach?: (documentId: Identifier, tagId: Identifier) => void;
onDocumentRename?: (documentId: Identifier, nextTitle: string) => Promise<boolean> | boolean;
tagLookupById?: Map<Identifier, DocumentTag> | null;
onTagClick?: (tagId: Identifier) => void;
onCorrespondentClick?: (correspondentId: Identifier) => void;
activeCorrespondentIdSet?: Set<Identifier> | null;
scrollRef?: RefObject<HTMLElement | null>;
// Desk specific (optional for now or handled via intersection)
viewId?: string | null;
activeTagFilters?: Array<Identifier | null>;
} }
const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = (props) => {
headerLeading = null, const {
onBreadcrumbNavigate, headerLeading = null,
currentFolderName, onBreadcrumbNavigate,
breadcrumbs, currentFolderName,
subfolders, breadcrumbs,
documents, subfolders,
searchResultIds, documents,
onFolderSelect, searchResultIds,
onFolderDrop, onRefresh = () => { },
onFolderDragOver, sortField,
onFolderDragLeave, sortDirection,
onFolderDragStart, onSortFieldChange,
onFolderDragEnd, onSortDirectionToggle,
draggedFolderId, onDeleteSelection,
onFolderRename, documentLookup,
draggingDocumentIds = [], tags,
onDocumentDragStart, correspondents,
onDocumentDragEnd, onBulkTagAdd,
onDocumentRename, onBulkTagRemove,
onEntryPointer = null, onBulkCorrespondentAdd,
tagLookupById, onBulkCorrespondentRemove,
activeCorrespondentIds = [], onBulkReanalyze,
ensureAssetUrl = null, folderOptions,
getDocumentAsset, onMoveDocumentsToFolder,
isSearchLoading = false, viewMode = 'list',
viewMode = 'list', onViewModeChange,
onViewModeChange, } = props;
onRefresh = () => { },
sortField, const {
sortDirection, assetContextValue,
onSortFieldChange, viewStateContextValue,
onSortDirectionToggle, commandContextValue,
onDeleteSelection, scrollRef,
documentLookup, hasDocumentEntries,
tags, } = useDocumentsContextValues(props);
correspondents,
onBulkTagAdd,
onBulkTagRemove,
onBulkCorrespondentAdd,
onBulkCorrespondentRemove,
onBulkReanalyze,
folderOptions,
onMoveDocumentsToFolder,
searchQuery = '',
activeTagFilters = [],
activeCorrespondentFilters = [],
selectedFolder = null,
onDocumentTagAttach,
onDocumentTagDetach,
}): ReactNode => {
const { const {
setFocusedEntryKey,
clearSelection, clearSelection,
} = useWorkspaceSelectionContext(); } = useWorkspaceSelectionContext();
const { const {
isActive: isFilterActive, isActive: isFilterActive,
includeDescendants, includeDescendants,
toggleIncludeDescendants, toggleIncludeDescendants,
toggleTag: toggleTagFilter,
toggleCorrespondent: toggleCorrespondentFilter,
} = useDocumentsFilter(); } = useDocumentsFilter();
const searchDocuments = useMemo( const searchDocuments = useMemo(
() => () =>
Array.isArray(searchResultIds) Array.isArray(searchResultIds)
? searchResultIds ? searchResultIds
.map((id) => documentLookup?.get?.(id) || null) .map((id: any) => documentLookup?.get?.(id) || null)
.filter((doc): doc is Record<string, unknown> => Boolean(doc)) .filter((doc: any): doc is Record<string, unknown> => Boolean(doc))
: null, : null,
[searchResultIds, documentLookup], [searchResultIds, documentLookup],
); );
@@ -159,28 +112,10 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
const rows = showingSearchResults && searchDocuments ? searchDocuments : documents; const rows = showingSearchResults && searchDocuments ? searchDocuments : documents;
const viewId = useMemo(() => {
if (showingSearchResults) {
const trimmedQuery = searchQuery.trim();
const tagsKey = [...activeTagFilters].sort().join(',');
const correspondentsKey = [...activeCorrespondentFilters].sort().join(',');
return `search:${trimmedQuery}|tags:${tagsKey}|corr:${correspondentsKey}`;
}
const folderKey = selectedFolder && selectedFolder !== '' ? selectedFolder : 'root';
return `folder:${folderKey}`;
}, [
showingSearchResults,
searchQuery,
activeTagFilters,
activeCorrespondentFilters,
selectedFolder,
]);
const headerTitle = showingSearchResults const headerTitle = showingSearchResults
? 'Search results' ? 'Search results'
: currentFolderName || 'Documents'; : currentFolderName || 'Documents';
const headerSubtitle = null;
const headerActions = useMemo( const headerActions = useMemo(
() => createDocumentsTableHeaderActions({ () => createDocumentsTableHeaderActions({
viewMode, viewMode,
@@ -211,7 +146,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
<SelectionFloatingPanel <SelectionFloatingPanel
documentLookup={documentLookup} documentLookup={documentLookup}
tags={tags} tags={tags}
tagLookupById={tagLookupById} tagLookupById={props.tagLookupById}
correspondents={correspondents} correspondents={correspondents}
onBulkTagAdd={onBulkTagAdd} onBulkTagAdd={onBulkTagAdd}
onBulkTagRemove={onBulkTagRemove} onBulkTagRemove={onBulkTagRemove}
@@ -226,7 +161,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
), [ ), [
documentLookup, documentLookup,
tags, tags,
tagLookupById, props.tagLookupById,
correspondents, correspondents,
onBulkTagAdd, onBulkTagAdd,
onBulkTagRemove, onBulkTagRemove,
@@ -240,14 +175,13 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
]); ]);
const headerConfig: DocumentsPanelHeaderConfig = useMemo(() => ({ const headerConfig: DocumentsPanelHeaderConfig = useMemo(() => ({
title: headerTitle, title: headerTitle,
subtitle: headerSubtitle, subtitle: null,
leading: headerLeading, leading: headerLeading,
actions: headerActions, actions: headerActions,
breadcrumbs, breadcrumbs,
floatingActions, floatingActions,
}), [ }), [
headerTitle, headerTitle,
headerSubtitle,
headerLeading, headerLeading,
headerActions, headerActions,
breadcrumbs, breadcrumbs,
@@ -265,7 +199,8 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
return trail[trail.length - 1]?.id || 'root'; return trail[trail.length - 1]?.id || 'root';
}, [breadcrumbs, showingSearchResults]); }, [breadcrumbs, showingSearchResults]);
const selectionContextRef = useRef(null); // Context marker logic for clearing selection on nav
const selectionContextRef = useRef<any>(null);
useEffect(() => { useEffect(() => {
const nextContext = showingSearchResults const nextContext = showingSearchResults
? { type: 'search', marker: searchResultIds } ? { type: 'search', marker: searchResultIds }
@@ -283,16 +218,16 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
}, [showingSearchResults, currentFolderId, searchResultIds, clearSelection]); }, [showingSearchResults, currentFolderId, searchResultIds, clearSelection]);
const entries = useMemo(() => { const entries = useMemo(() => {
const list = []; const list: DocumentsListEntry[] = []; // Explicit type
if (!showingSearchResults) { if (!showingSearchResults) {
subfolders.forEach((folder) => { subfolders.forEach((folder: any) => {
if (!folder || !folder.id) { if (!folder || !folder.id) {
return; return;
} }
list.push({ type: EntryType.folder, id: folder.id, key: `folder:${folder.id}`, folder }); list.push({ type: EntryType.folder, id: folder.id, key: `folder:${folder.id}`, folder });
}); });
} }
rows.forEach((doc) => { rows.forEach((doc: any) => {
if (!doc || !doc.id) { if (!doc || !doc.id) {
return; return;
} }
@@ -301,143 +236,11 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
return list; return list;
}, [showingSearchResults, subfolders, rows]); }, [showingSearchResults, subfolders, rows]);
const draggingSet = useMemo(
() => new Set(draggingDocumentIds || []),
[draggingDocumentIds],
);
const activeCorrespondentIdSet = useMemo(
() => new Set(activeCorrespondentIds || []),
[activeCorrespondentIds],
);
const scrollRef = useRef<HTMLElement | null>(null);
const suppressDocumentClickRef = useRef(false);
const isGridView = viewMode === 'grid'; const isGridView = viewMode === 'grid';
const isDeskView = viewMode === 'desk'; const isDeskView = viewMode === 'desk';
const isTagDragEvent = useCallback((event) => isTagTransferEvent(event), []);
const draggingTagRef = useRef<{ docId: Identifier; tagId: Identifier } | null>(null);
const handleDocumentTagDragStart = useCallback(
(_event, docId, tagId) => {
draggingTagRef.current = { docId, tagId };
},
[],
);
const handleDocumentTagDragEnd = useCallback(
(_event) => {
draggingTagRef.current = null;
},
[],
);
const handleDocumentTagDragOver = useCallback(
(event, docId) => {
if (!isTagDragEvent(event)) {
return;
}
event.preventDefault();
const isSource = draggingTagRef.current?.docId === docId;
event.dataTransfer.dropEffect = isSource ? 'copy' : 'move';
event.currentTarget.classList.add('tag-drop-target');
},
[isTagDragEvent],
);
const handleDocumentTagDragLeave = useCallback(
(event) => {
if (!isTagDragEvent(event)) {
return;
}
if (event.relatedTarget && event.currentTarget.contains(event.relatedTarget)) {
return;
}
event.currentTarget.classList.remove('tag-drop-target');
},
[isTagDragEvent],
);
const handleFolderClick = useCallback(
(folder, event) => {
if (!folder) {
return;
}
if (onEntryPointer) {
onEntryPointer(
{ type: EntryType.folder, id: folder.id, key: `folder:${folder.id}`, folder },
event,
);
}
if (
!isPointerModifierEvent(event)
&& isPrimaryPointerEvent(event)
&& scrollRef.current
) {
scrollRef.current.focus({ preventScroll: true });
setFocusedEntryKey(`folder:${folder.id}`);
}
},
[onEntryPointer, setFocusedEntryKey],
);
const handleDocumentDragStartLocal = useCallback(
(event, doc) => {
suppressDocumentClickRef.current = true;
onDocumentDragStart?.(event, doc);
},
[onDocumentDragStart],
);
const handleDocumentDragEndLocal = useCallback(
(event) => {
onDocumentDragEnd?.(event);
requestAnimationFrame(() => {
suppressDocumentClickRef.current = false;
});
},
[onDocumentDragEnd],
);
const hasDocumentEntries = useMemo(
() => entries.some((entry) => entry.type === EntryType.document),
[entries],
);
const viewProps = { const viewProps = {
entries, entries,
draggingDocumentIdsSet: draggingSet,
draggedFolderId,
onFolderClick: handleFolderClick,
onFolderSelect,
onFolderDragOver,
onFolderDragLeave,
onFolderDrop,
onFolderDragStart,
onFolderDragEnd,
onFolderRename,
onDocumentDragStart: handleDocumentDragStartLocal,
onDocumentDragEnd: handleDocumentDragEndLocal,
onDocumentTagDragStart: handleDocumentTagDragStart,
onDocumentTagDragEnd: handleDocumentTagDragEnd,
onDocumentTagDragOver: handleDocumentTagDragOver,
onDocumentTagDragLeave: handleDocumentTagDragLeave,
onDocumentTagAttach,
onDocumentTagDetach,
onDocumentRename,
ensureAssetUrl,
getDocumentAsset,
tagLookupById,
onTagClick: toggleTagFilter,
scrollRef,
activeCorrespondentIdSet: activeCorrespondentIdSet,
onCorrespondentClick: toggleCorrespondentFilter,
viewId,
onEntryPointer,
}; };
const [iconSizes] = useState({ const [iconSizes] = useState({
@@ -446,6 +249,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
desk: DEFAULT_DESKTOP_CARD_SIZE, desk: DEFAULT_DESKTOP_CARD_SIZE,
}); });
const isSearchLoading = props.isSearchLoading || false;
const renderBody = () => { const renderBody = () => {
const hasEntries = entries.length > 0; const hasEntries = entries.length > 0;
@@ -489,21 +293,25 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
}, [shouldHandlePanelInteractions, clearSelection]); }, [shouldHandlePanelInteractions, clearSelection]);
return ( return (
<> <DocumentsAssetContext.Provider value={assetContextValue}>
<DocumentsPanelHeader <DocumentsViewStateContext.Provider value={viewStateContextValue}>
header={headerConfig} <DocumentsCommandContext.Provider value={commandContextValue}>
onBreadcrumbClick={onBreadcrumbNavigate} <DocumentsPanelHeader
/> header={headerConfig}
<div className="documents-panel-wrapper"> onBreadcrumbClick={onBreadcrumbNavigate}
<section />
ref={scrollRef} <div className="documents-panel-wrapper">
className={`documents-panel documents-panel--view-${panelVariant}`} <section
onClick={handleSectionClick} ref={scrollRef}
> className={`documents-panel documents-panel--view-${panelVariant}`}
{renderBody()} onClick={handleSectionClick}
</section> >
</div> {renderBody()}
</> </section>
</div>
</DocumentsCommandContext.Provider>
</DocumentsViewStateContext.Provider>
</DocumentsAssetContext.Provider>
); );
}; };
@@ -0,0 +1,260 @@
import { useMemo, useCallback, useRef } from 'react';
import type { DocumentsPanelInnerProps } from './DocumentsPanel';
import { isTagTransferEvent } from '../features/tagging/tagTransfer';
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../features/selection/useEntryPointer';
import { useDocumentsFilter } from '../context/DocumentsFilterContext';
import { useWorkspaceSelectionContext } from '../../app/WorkspaceSelectionContext';
import type { Identifier } from '../../types/identifiers';
const EntryType = {
folder: 'folder',
document: 'document',
};
export const useDocumentsContextValues = (props: DocumentsPanelInnerProps) => {
const {
ensureAssetUrl,
getDocumentAsset,
isSearchLoading,
searchQuery = '',
activeTagFilters = [],
activeCorrespondentFilters = [],
selectedFolder = null,
onDocumentDragStart,
onDocumentDragEnd,
onEntryPointer,
activeCorrespondentIds = [],
draggingDocumentIds = [],
tagLookupById,
} = props;
const {
setFocusedEntryKey,
} = useWorkspaceSelectionContext();
const {
toggleTag: toggleTagFilter,
toggleCorrespondent: toggleCorrespondentFilter,
} = useDocumentsFilter();
// Derived State
const draggingDocumentIdsSet = useMemo(
() => new Set(draggingDocumentIds || []),
[draggingDocumentIds],
);
const activeCorrespondentIdSet = useMemo(
() => new Set(activeCorrespondentIds || []),
[activeCorrespondentIds],
);
const showingSearchResults = Array.isArray(props.searchResultIds);
const hasDocumentEntries = (props.documents || []).length > 0 || (showingSearchResults && (props.searchResultIds || []).length > 0);
const viewId = useMemo(() => {
if (showingSearchResults) {
const trimmedQuery = searchQuery.trim();
const tagsKey = [...activeTagFilters].sort().join(',');
const correspondentsKey = [...activeCorrespondentFilters].sort().join(',');
return `search:${trimmedQuery}|tags:${tagsKey}|corr:${correspondentsKey}`;
}
const folderKey = selectedFolder && selectedFolder !== '' ? selectedFolder : 'root';
return `folder:${folderKey}`;
}, [
showingSearchResults,
searchQuery,
activeTagFilters,
activeCorrespondentFilters,
selectedFolder,
]);
// Refs
const scrollRef = useRef<HTMLElement | null>(null);
const suppressDocumentClickRef = useRef(false);
const draggingTagRef = useRef<{ docId: Identifier; tagId: Identifier } | null>(null);
// Handlers
const isTagDragEvent = useCallback((event: any) => isTagTransferEvent(event), []);
const handleDocumentTagDragStart = useCallback(
(_event: any, docId: Identifier, tagId: Identifier) => {
draggingTagRef.current = { docId, tagId };
},
[],
);
const handleDocumentTagDragEnd = useCallback(
(_event: any) => {
draggingTagRef.current = null;
},
[],
);
const handleDocumentTagDragOver = useCallback(
(event: any, docId: Identifier) => {
if (!isTagDragEvent(event)) {
return;
}
event.preventDefault();
const isSource = draggingTagRef.current?.docId === docId;
event.dataTransfer.dropEffect = isSource ? 'copy' : 'move';
event.currentTarget.classList.add('tag-drop-target');
},
[isTagDragEvent],
);
const handleDocumentTagDragLeave = useCallback(
(event: any) => {
if (!isTagDragEvent(event)) {
return;
}
if (event.relatedTarget && event.currentTarget.contains(event.relatedTarget)) {
return;
}
event.currentTarget.classList.remove('tag-drop-target');
},
[isTagDragEvent],
);
const handleFolderClick = useCallback(
(folder: any, event: any) => {
if (!folder) {
return;
}
if (onEntryPointer) {
onEntryPointer(
{ type: EntryType.folder, id: folder.id, key: `folder:${folder.id}`, folder },
event,
);
}
if (
!isPointerModifierEvent(event)
&& isPrimaryPointerEvent(event)
&& scrollRef.current
) {
scrollRef.current.focus({ preventScroll: true });
setFocusedEntryKey(`folder:${folder.id}`);
}
},
[onEntryPointer, setFocusedEntryKey],
);
const handleDocumentDragStartLocal = useCallback(
(event: any, doc: any) => {
suppressDocumentClickRef.current = true;
onDocumentDragStart?.(event, doc);
},
[onDocumentDragStart],
);
const handleDocumentDragEndLocal = useCallback(
(event: any) => {
onDocumentDragEnd?.(event);
requestAnimationFrame(() => {
suppressDocumentClickRef.current = false;
});
},
[onDocumentDragEnd],
);
// Context Values Construction
const assetContextValue = useMemo(() => ({
ensureAssetUrl,
getDocumentAsset,
}), [ensureAssetUrl, getDocumentAsset]);
const viewStateContextValue = useMemo(() => ({
viewId,
scrollRef,
tagLookupById,
activeCorrespondentIdSet,
draggingDocumentIdsSet,
draggedFolderId: props.draggedFolderId,
}), [
viewId,
scrollRef,
tagLookupById,
activeCorrespondentIdSet,
draggingDocumentIdsSet,
props.draggedFolderId,
]);
// Use refs to stabilize handlers and avoid massive dependency arrays
const latestPropsRef = useRef(props);
const latestHandlersRef = useRef({
handleFolderClick,
handleDocumentDragStartLocal,
handleDocumentDragEndLocal,
handleDocumentTagDragStart,
handleDocumentTagDragEnd,
handleDocumentTagDragOver,
handleDocumentTagDragLeave,
toggleTagFilter,
toggleCorrespondentFilter,
});
// Update refs on every render
latestPropsRef.current = props;
latestHandlersRef.current = {
handleFolderClick,
handleDocumentDragStartLocal,
handleDocumentDragEndLocal,
handleDocumentTagDragStart,
handleDocumentTagDragEnd,
handleDocumentTagDragOver,
handleDocumentTagDragLeave,
toggleTagFilter,
toggleCorrespondentFilter,
};
const commandContextValue = useMemo(() => ({
folder: {
onClick: (f: any, e: any) => latestHandlersRef.current.handleFolderClick(f, e),
onSelect: (id: Identifier) => latestPropsRef.current.onFolderSelect?.(id),
onRename: (id: Identifier, name: string) => latestPropsRef.current.onFolderRename?.(id, name),
onDrag: {
start: (e: any, f: any) => latestPropsRef.current.onFolderDragStart?.(e, f),
end: (e: any) => latestPropsRef.current.onFolderDragEnd?.(e),
over: (e: any, f: any) => latestPropsRef.current.onFolderDragOver?.(e, f),
leave: (e: any) => latestPropsRef.current.onFolderDragLeave?.(e),
drop: (e: any, f: any) => latestPropsRef.current.onFolderDrop?.(e, f),
},
},
document: {
onRename: (id: Identifier, name: string) => latestPropsRef.current.onDocumentRename?.(id, name),
onDrag: {
start: (e: any, d: any) => latestHandlersRef.current.handleDocumentDragStartLocal(e, d),
end: (e: any) => latestHandlersRef.current.handleDocumentDragEndLocal(e),
},
},
tags: {
onDrag: {
start: (e: any, d: any, t: any) => latestHandlersRef.current.handleDocumentTagDragStart(e, d, t),
end: (e: any) => latestHandlersRef.current.handleDocumentTagDragEnd(e),
over: (e: any, d: any) => latestHandlersRef.current.handleDocumentTagDragOver(e, d),
leave: (e: any) => latestHandlersRef.current.handleDocumentTagDragLeave(e),
},
onAttach: (d: any, t: any) => latestPropsRef.current.onDocumentTagAttach?.(d, t),
onDetach: (d: any, t: any) => latestPropsRef.current.onDocumentTagDetach?.(d, t),
onClick: (t: any) => latestHandlersRef.current.toggleTagFilter(t),
},
correspondents: {
onClick: (c: any) => latestHandlersRef.current.toggleCorrespondentFilter(c),
},
onEntryPointer: (entry: any, e: any) => latestPropsRef.current.onEntryPointer?.(entry, e),
}), []); // Stable forever!
return {
assetContextValue,
viewStateContextValue,
commandContextValue,
scrollRef,
hasDocumentEntries,
isSearchLoading,
showingSearchResults,
activeTagFilters,
activeCorrespondentFilters,
};
};
-5
View File
@@ -100,11 +100,6 @@ type DocumentEntry = {
export type DocumentsListEntry = FolderEntry | DocumentEntry; export type DocumentsListEntry = FolderEntry | DocumentEntry;
// Event Handlers
import type { MouseEvent } from 'react';
export type FolderEventHandler = (folder: Folder, event: MouseEvent<HTMLElement>) => void;
/** /**
* Represents a folder node in the UI tree structure (flat map representation). * Represents a folder node in the UI tree structure (flat map representation).
*/ */