diff --git a/frontend/src/documents/DocumentsView.tsx b/frontend/src/documents/DocumentsView.tsx index 5743ea5..e2705a3 100644 --- a/frontend/src/documents/DocumentsView.tsx +++ b/frontend/src/documents/DocumentsView.tsx @@ -8,6 +8,8 @@ import DocumentsGridCard from './components/DocumentsGridCard'; import DocumentsListContainer from './components/DocumentsListContainer'; import DocumentsGridContainer from './components/DocumentsGridContainer'; import type { DocumentsViewProps } from './panel/DocumentsPanel'; +import { useDocumentsViewStateContext } from './context/DocumentsViewStateContext'; +import { useDocumentsCommandContext } from './context/DocumentsCommandContext'; interface AbstractDocumentsViewProps void; children: React.ReactNode }> extends DocumentsViewProps { ContainerComponent: React.ComponentType; @@ -22,7 +24,16 @@ const AbstractDocumentsView = void; chil containerProps, ...props }: AbstractDocumentsViewProps) => { - 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({ onDocumentRename, onFolderRename, @@ -30,8 +41,8 @@ const AbstractDocumentsView = void; chil const { handleKeyDown, handleFocus } = useDocumentsNavigation({ entries, onFolderSelect, - viewMode: props.viewMode, - scrollRef: props.scrollRef, + viewMode: viewMode || props.viewMode, + scrollRef: scrollRef, }); const { clearSelection } = viewLogic; diff --git a/frontend/src/documents/components/DocumentEntry.tsx b/frontend/src/documents/components/DocumentEntry.tsx index cfb1d15..1d339c9 100644 --- a/frontend/src/documents/components/DocumentEntry.tsx +++ b/frontend/src/documents/components/DocumentEntry.tsx @@ -1,10 +1,9 @@ import React from 'react'; -import type { DocumentsViewProps } from '../panel/DocumentsPanel'; import type { DocumentViewLogic } from '../logic/useDocumentViewLogic'; import { useDocumentItemLogic } from '../logic/useDocumentItemLogic'; import EntryShell from './EntryShell'; -interface DocumentEntryProps extends DocumentsViewProps { +interface DocumentEntryProps { doc: any; viewLogic: DocumentViewLogic; component: React.ElementType; @@ -15,7 +14,7 @@ interface DocumentEntryProps extends DocumentsViewProps { const DocumentEntry: React.FC = (props) => { const { doc, component, className, role, children, viewLogic } = props; - const logic = useDocumentItemLogic({ doc, viewLogic, ...props }); + const logic = useDocumentItemLogic({ doc, viewLogic }); return ( = (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') { const folder = entry.folder; @@ -26,8 +34,8 @@ const DocumentsGridCard: React.FC = (props) => { return ( = (props) => { return ( = (props) => { <>
= (props) => {
@@ -128,4 +136,5 @@ const DocumentsGridCard: React.FC = (props) => { ); }; + export default DocumentsGridCard; diff --git a/frontend/src/documents/components/DocumentsListRow.tsx b/frontend/src/documents/components/DocumentsListRow.tsx index c0c30b9..09670a5 100644 --- a/frontend/src/documents/components/DocumentsListRow.tsx +++ b/frontend/src/documents/components/DocumentsListRow.tsx @@ -4,7 +4,9 @@ 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 { useDocumentsAssetContext } from '../context/DocumentsAssetContext'; +import { useDocumentsViewStateContext } from '../context/DocumentsViewStateContext'; +import { useDocumentsCommandContext } from '../context/DocumentsCommandContext'; import type { DocumentViewLogic } from '../logic/useDocumentViewLogic'; import EditableEntryTitle from './EditableEntryTitle'; import EntryCorrespondents from './EntryCorrespondents'; @@ -12,7 +14,7 @@ import EntryTags from './EntryTags'; import FolderEntry from './FolderEntry'; import DocumentEntry from './DocumentEntry'; -interface DocumentsListRowProps extends DocumentsViewProps { +interface DocumentsListRowProps { entry: DocumentsListEntry; viewLogic: DocumentViewLogic; iconSize?: number; @@ -20,6 +22,12 @@ interface DocumentsListRowProps extends DocumentsViewProps { const DocumentsListRow: React.FC = (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') { const folder = entry.folder; @@ -27,8 +35,8 @@ const DocumentsListRow: React.FC = (props) => { return ( @@ -79,8 +87,8 @@ const DocumentsListRow: React.FC = (props) => { return ( @@ -89,10 +97,10 @@ const DocumentsListRow: React.FC = (props) => { @@ -102,8 +110,8 @@ const DocumentsListRow: React.FC = (props) => { = (props) => {
@@ -145,4 +153,5 @@ const DocumentsListRow: React.FC = (props) => { ); }; + export default DocumentsListRow; diff --git a/frontend/src/documents/components/FolderEntry.tsx b/frontend/src/documents/components/FolderEntry.tsx index a963b89..e092aef 100644 --- a/frontend/src/documents/components/FolderEntry.tsx +++ b/frontend/src/documents/components/FolderEntry.tsx @@ -1,10 +1,9 @@ import React from 'react'; -import type { DocumentsViewProps } from '../panel/DocumentsPanel'; import type { DocumentViewLogic } from '../logic/useDocumentViewLogic'; import { useFolderItemLogic } from '../features/folders/useFolderItemLogic'; import EntryShell from './EntryShell'; -interface FolderEntryProps extends DocumentsViewProps { +interface FolderEntryProps { folder: any; viewLogic: DocumentViewLogic; component: React.ElementType; @@ -15,7 +14,7 @@ interface FolderEntryProps extends DocumentsViewProps { const FolderEntry: React.FC = (props) => { const { folder, component, className, role, children, viewLogic } = props; - const logic = useFolderItemLogic({ folder, viewLogic, ...props }); + const logic = useFolderItemLogic({ folder, viewLogic }); return ( unknown; + getDocumentAsset?: (...args: any[]) => unknown; +} + +export const DocumentsAssetContext = createContext({}); + +export const useDocumentsAssetContext = () => useContext(DocumentsAssetContext); diff --git a/frontend/src/documents/context/DocumentsCommandContext.tsx b/frontend/src/documents/context/DocumentsCommandContext.tsx new file mode 100644 index 0000000..6c3269e --- /dev/null +++ b/frontend/src/documents/context/DocumentsCommandContext.tsx @@ -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; + onDrag: { + start?: (event: DragEvent, folderId: Identifier | 'root') => void; + end?: (event: DragEvent) => void; + over?: (event: DragEvent, folderId: Identifier | 'root') => void; + leave?: (event: DragEvent) => void; + drop?: (event: DragEvent, folderId: Identifier | 'root') => void; + }; + }; + document: { + onRename?: (documentId: Identifier, nextTitle: string) => Promise | boolean; + onDrag: { + start?: (event: DragEvent, document: Document) => void; + end?: (event: DragEvent) => void; + }; + }; + tags: { + onDrag: { + start?: (event: DragEvent, docId: Identifier, tagId: Identifier) => void; + end?: (event: DragEvent) => void; + over?: (event: DragEvent, docId: Identifier) => void; + leave?: (event: DragEvent) => 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({ + folder: { onDrag: {} }, + document: { onDrag: {} }, + tags: { onDrag: {} }, + correspondents: {}, +}); + +export const useDocumentsCommandContext = () => useContext(DocumentsCommandContext); diff --git a/frontend/src/documents/context/DocumentsViewStateContext.tsx b/frontend/src/documents/context/DocumentsViewStateContext.tsx new file mode 100644 index 0000000..3428ec8 --- /dev/null +++ b/frontend/src/documents/context/DocumentsViewStateContext.tsx @@ -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; + tagLookupById?: Map | null; + activeCorrespondentIdSet?: Set | null; + draggingDocumentIdsSet?: Set | null; + draggedFolderId?: Identifier | 'root' | null; +} + +export const DocumentsViewStateContext = createContext({}); + +export const useDocumentsViewStateContext = () => useContext(DocumentsViewStateContext); diff --git a/frontend/src/documents/features/folders/useFolderItemLogic.ts b/frontend/src/documents/features/folders/useFolderItemLogic.ts index 053192f..8e42ea0 100644 --- a/frontend/src/documents/features/folders/useFolderItemLogic.ts +++ b/frontend/src/documents/features/folders/useFolderItemLogic.ts @@ -1,26 +1,34 @@ import React, { type DragEvent } from 'react'; -import type { DocumentsViewProps } from '../../panel/DocumentsPanel'; import type { DocumentViewLogic } from '../../logic/useDocumentViewLogic'; +import { useDocumentsCommandContext } from '../../context/DocumentsCommandContext'; +import { useDocumentsViewStateContext } from '../../context/DocumentsViewStateContext'; -interface UseFolderItemLogicProps extends DocumentsViewProps { +interface UseFolderItemLogicProps { folder: any; viewLogic: DocumentViewLogic; } export const useFolderItemLogic = (props: UseFolderItemLogicProps) => { + const { folder, viewLogic } = props; const { - folder, - viewLogic, draggedFolderId, - onFolderClick, - onFolderSelect, - onFolderDragOver, - onFolderDragLeave, - onFolderDrop, - onFolderDragStart, - onFolderDragEnd, - onFolderRename, - } = props; + } = useDocumentsViewStateContext(); + + const { + folder: { + onClick: onFolderClick, + onSelect: onFolderSelect, + onRename: onFolderRename, + onDrag: { + start: onFolderDragStart, + end: onFolderDragEnd, + over: onFolderDragOver, + leave: onFolderDragLeave, + drop: onFolderDrop, + } + } + + } = useDocumentsCommandContext(); const { selectedFolderIdsSet, @@ -92,3 +100,5 @@ export const useFolderItemLogic = (props: UseFolderItemLogicProps) => { handlers, }; }; + + diff --git a/frontend/src/documents/logic/useDocumentItemLogic.ts b/frontend/src/documents/logic/useDocumentItemLogic.ts index 373714b..71602b9 100644 --- a/frontend/src/documents/logic/useDocumentItemLogic.ts +++ b/frontend/src/documents/logic/useDocumentItemLogic.ts @@ -3,30 +3,38 @@ import { parseTagTransferPayload } from '../features/tagging/tagTransfer'; import { createDocumentEntryKey } from '../../app/entryKey'; import { useDocumentOpen } from '../../lib/context/DocumentOpenContext'; 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 { Identifier } from '../../types/identifiers'; -interface UseDocumentItemLogicProps extends DocumentsViewProps { +interface UseDocumentItemLogicProps { doc: Document; viewLogic: DocumentViewLogic; - onEntryPointer?: (entry: any, event: any) => void; } export const useDocumentItemLogic = (props: UseDocumentItemLogicProps) => { + const { doc, viewLogic } = props; const { - doc, - viewLogic, - draggingDocumentIdsSet, - onDocumentDragStart, - onDocumentDragEnd, - onDocumentTagDragStart, - onDocumentTagDragEnd, - onDocumentTagDragOver, - onDocumentTagDragLeave, - onDocumentTagAttach, - onDocumentRename, - } = props; + draggingDocumentIdsSet + } = useDocumentsViewStateContext(); + + const { + document: { + onDrag: { start: onDocumentDragStart, end: onDocumentDragEnd }, + onRename: onDocumentRename + }, + tags: { + onDrag: { + start: onDocumentTagDragStart, + end: onDocumentTagDragEnd, + over: onDocumentTagDragOver, + leave: onDocumentTagDragLeave, + }, + onAttach: onDocumentTagAttach + }, + onEntryPointer + } = useDocumentsCommandContext(); const { selectedDocumentIdsSet, @@ -58,8 +66,8 @@ export const useDocumentItemLogic = (props: UseDocumentItemLogicProps) => { const handlers = { onClick: (event: React.MouseEvent) => { - if (props.onEntryPointer) { - props.onEntryPointer({ type: 'document', id: doc.id, key: createDocumentEntryKey(doc.id), document: doc }, event); + if (onEntryPointer) { + onEntryPointer({ type: 'document', id: doc.id, key: createDocumentEntryKey(doc.id), document: doc }, event); } else { const key = createDocumentEntryKey(doc.id); handleEntrySelection(key, event); @@ -106,3 +114,5 @@ export const useDocumentItemLogic = (props: UseDocumentItemLogicProps) => { handlers, }; }; + + diff --git a/frontend/src/documents/panel/DocumentsPanel.tsx b/frontend/src/documents/panel/DocumentsPanel.tsx index a6688ab..cdcf4d3 100644 --- a/frontend/src/documents/panel/DocumentsPanel.tsx +++ b/frontend/src/documents/panel/DocumentsPanel.tsx @@ -6,21 +6,16 @@ import React, { useEffect, } from 'react'; import { DocumentsList, DocumentsGrid } from '../DocumentsView'; -import type { DragEvent, ReactNode, RefObject } from 'react'; +import type { ReactNode } from 'react'; import type { DocumentsListEntry, - FolderEventHandler, - Document, - DocumentTag, } from '../../types/documents'; import DesktopWorkspace from '../../desktop/components/DesktopWorkspace'; -import { isTagTransferEvent } from '../features/tagging/tagTransfer'; -import { isPointerModifierEvent, isPrimaryPointerEvent } from '../features/selection/useEntryPointer'; import { WorkspaceSelectionProvider, useWorkspaceSelectionContext, + type WorkspaceSelectionValue, } from '../../app/WorkspaceSelectionContext'; -import type { WorkspaceSelectionValue } from '../../app/WorkspaceSelectionContext'; import DocumentsPanelHeader, { DocumentsPanelHeaderConfig, DocumentsHeaderBreadcrumb, @@ -33,14 +28,17 @@ import { DEFAULT_LIST_ICON_SIZE, DEFAULT_DESKTOP_CARD_SIZE, } 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 = { - folder: 'folder', - document: 'document', + folder: 'folder' as const, + document: 'document' as const, }; -interface DocumentsPanelInnerProps { +export interface DocumentsPanelInnerProps { headerLeading?: ReactNode; onBreadcrumbNavigate?: (crumb: DocumentsHeaderBreadcrumb) => void; [key: string]: any; @@ -52,105 +50,60 @@ interface DocumentsPanelProps extends DocumentsPanelInnerProps { export interface DocumentsViewProps { entries: DocumentsListEntry[]; - draggingDocumentIdsSet?: Set | null; - draggedFolderId?: Identifier | 'root' | null; - ensureAssetUrl?: (...args: any[]) => unknown; - getDocumentAsset?: (...args: any[]) => unknown; - onFolderClick?: FolderEventHandler; - onFolderSelect?: (folderId: Identifier | 'root') => void; - onFolderDragOver?: (event: DragEvent, folderId: Identifier | 'root') => void; - onFolderDragLeave?: (event: DragEvent) => void; - onFolderDrop?: (event: DragEvent, folderId: Identifier | 'root') => void; - onFolderDragStart?: (event: DragEvent, folderId: Identifier | 'root') => void; - onFolderDragEnd?: (event: DragEvent) => void; - onFolderRename?: (folderId: Identifier | 'root', nextName: string) => Promise | boolean; - onDocumentDragStart?: (event: DragEvent, document: Document) => void; - onDocumentDragEnd?: (event: DragEvent) => void; - onDocumentTagDragStart?: (event: DragEvent, docId: Identifier, tagId: Identifier) => void; - onDocumentTagDragEnd?: (event: DragEvent) => void; - onDocumentTagDragOver?: (event: DragEvent, docId: Identifier) => void; - onDocumentTagDragLeave?: (event: DragEvent) => void; - onDocumentTagAttach?: (documentId: Identifier, tagId: Identifier) => void; - onDocumentTagDetach?: (documentId: Identifier, tagId: Identifier) => void; - onDocumentRename?: (documentId: Identifier, nextTitle: string) => Promise | boolean; - tagLookupById?: Map | null; - onTagClick?: (tagId: Identifier) => void; - onCorrespondentClick?: (correspondentId: Identifier) => void; - activeCorrespondentIdSet?: Set | null; - scrollRef?: RefObject; - // Desk specific (optional for now or handled via intersection) - viewId?: string | null; - activeTagFilters?: Array; } -const DocumentsPanelInner: React.FC = ({ - headerLeading = null, - onBreadcrumbNavigate, - currentFolderName, - breadcrumbs, - subfolders, - documents, - searchResultIds, - onFolderSelect, - onFolderDrop, - onFolderDragOver, - onFolderDragLeave, - onFolderDragStart, - onFolderDragEnd, - draggedFolderId, - onFolderRename, - draggingDocumentIds = [], - onDocumentDragStart, - onDocumentDragEnd, - onDocumentRename, - onEntryPointer = null, - tagLookupById, - activeCorrespondentIds = [], - ensureAssetUrl = null, - getDocumentAsset, - isSearchLoading = false, - viewMode = 'list', - onViewModeChange, - onRefresh = () => { }, - sortField, - sortDirection, - onSortFieldChange, - onSortDirectionToggle, - onDeleteSelection, - documentLookup, - tags, - correspondents, - onBulkTagAdd, - onBulkTagRemove, - onBulkCorrespondentAdd, - onBulkCorrespondentRemove, - onBulkReanalyze, - folderOptions, - onMoveDocumentsToFolder, - searchQuery = '', - activeTagFilters = [], - activeCorrespondentFilters = [], - selectedFolder = null, - onDocumentTagAttach, - onDocumentTagDetach, -}): ReactNode => { +const DocumentsPanelInner: React.FC = (props) => { + const { + headerLeading = null, + onBreadcrumbNavigate, + currentFolderName, + breadcrumbs, + subfolders, + documents, + searchResultIds, + onRefresh = () => { }, + sortField, + sortDirection, + onSortFieldChange, + onSortDirectionToggle, + onDeleteSelection, + documentLookup, + tags, + correspondents, + onBulkTagAdd, + onBulkTagRemove, + onBulkCorrespondentAdd, + onBulkCorrespondentRemove, + onBulkReanalyze, + folderOptions, + onMoveDocumentsToFolder, + viewMode = 'list', + onViewModeChange, + } = props; + + const { + assetContextValue, + viewStateContextValue, + commandContextValue, + scrollRef, + hasDocumentEntries, + } = useDocumentsContextValues(props); + const { - setFocusedEntryKey, clearSelection, } = useWorkspaceSelectionContext(); const { isActive: isFilterActive, includeDescendants, toggleIncludeDescendants, - toggleTag: toggleTagFilter, - toggleCorrespondent: toggleCorrespondentFilter, } = useDocumentsFilter(); + const searchDocuments = useMemo( () => Array.isArray(searchResultIds) ? searchResultIds - .map((id) => documentLookup?.get?.(id) || null) - .filter((doc): doc is Record => Boolean(doc)) + .map((id: any) => documentLookup?.get?.(id) || null) + .filter((doc: any): doc is Record => Boolean(doc)) : null, [searchResultIds, documentLookup], ); @@ -159,28 +112,10 @@ const DocumentsPanelInner: React.FC = ({ 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 ? 'Search results' : currentFolderName || 'Documents'; - const headerSubtitle = null; + const headerActions = useMemo( () => createDocumentsTableHeaderActions({ viewMode, @@ -211,7 +146,7 @@ const DocumentsPanelInner: React.FC = ({ = ({ ), [ documentLookup, tags, - tagLookupById, + props.tagLookupById, correspondents, onBulkTagAdd, onBulkTagRemove, @@ -240,14 +175,13 @@ const DocumentsPanelInner: React.FC = ({ ]); const headerConfig: DocumentsPanelHeaderConfig = useMemo(() => ({ title: headerTitle, - subtitle: headerSubtitle, + subtitle: null, leading: headerLeading, actions: headerActions, breadcrumbs, floatingActions, }), [ headerTitle, - headerSubtitle, headerLeading, headerActions, breadcrumbs, @@ -265,7 +199,8 @@ const DocumentsPanelInner: React.FC = ({ return trail[trail.length - 1]?.id || 'root'; }, [breadcrumbs, showingSearchResults]); - const selectionContextRef = useRef(null); + // Context marker logic for clearing selection on nav + const selectionContextRef = useRef(null); useEffect(() => { const nextContext = showingSearchResults ? { type: 'search', marker: searchResultIds } @@ -283,16 +218,16 @@ const DocumentsPanelInner: React.FC = ({ }, [showingSearchResults, currentFolderId, searchResultIds, clearSelection]); const entries = useMemo(() => { - const list = []; + const list: DocumentsListEntry[] = []; // Explicit type if (!showingSearchResults) { - subfolders.forEach((folder) => { + subfolders.forEach((folder: any) => { if (!folder || !folder.id) { return; } list.push({ type: EntryType.folder, id: folder.id, key: `folder:${folder.id}`, folder }); }); } - rows.forEach((doc) => { + rows.forEach((doc: any) => { if (!doc || !doc.id) { return; } @@ -301,143 +236,11 @@ const DocumentsPanelInner: React.FC = ({ return list; }, [showingSearchResults, subfolders, rows]); - const draggingSet = useMemo( - () => new Set(draggingDocumentIds || []), - [draggingDocumentIds], - ); - const activeCorrespondentIdSet = useMemo( - () => new Set(activeCorrespondentIds || []), - [activeCorrespondentIds], - ); - const scrollRef = useRef(null); - const suppressDocumentClickRef = useRef(false); const isGridView = viewMode === 'grid'; 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 = { 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({ @@ -446,6 +249,7 @@ const DocumentsPanelInner: React.FC = ({ desk: DEFAULT_DESKTOP_CARD_SIZE, }); + const isSearchLoading = props.isSearchLoading || false; const renderBody = () => { const hasEntries = entries.length > 0; @@ -489,21 +293,25 @@ const DocumentsPanelInner: React.FC = ({ }, [shouldHandlePanelInteractions, clearSelection]); return ( - <> - -
-
- {renderBody()} -
-
- + + + + +
+
+ {renderBody()} +
+
+
+
+
); }; diff --git a/frontend/src/documents/panel/useDocumentsContextValues.ts b/frontend/src/documents/panel/useDocumentsContextValues.ts new file mode 100644 index 0000000..9a6b4a2 --- /dev/null +++ b/frontend/src/documents/panel/useDocumentsContextValues.ts @@ -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(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, + }; +}; diff --git a/frontend/src/types/documents.ts b/frontend/src/types/documents.ts index 83001c1..e43d48a 100644 --- a/frontend/src/types/documents.ts +++ b/frontend/src/types/documents.ts @@ -100,11 +100,6 @@ type DocumentEntry = { export type DocumentsListEntry = FolderEntry | DocumentEntry; -// Event Handlers -import type { MouseEvent } from 'react'; - -export type FolderEventHandler = (folder: Folder, event: MouseEvent) => void; - /** * Represents a folder node in the UI tree structure (flat map representation). */