refactor(documents): encapsulate panel logic and state into dedicated React contexts
This commit is contained in:
@@ -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<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> = ({
|
||||
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<DocumentsPanelInnerProps> = (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<string, unknown> => Boolean(doc))
|
||||
.map((id: any) => documentLookup?.get?.(id) || null)
|
||||
.filter((doc: any): doc is Record<string, unknown> => Boolean(doc))
|
||||
: null,
|
||||
[searchResultIds, documentLookup],
|
||||
);
|
||||
@@ -159,28 +112,10 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
||||
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<DocumentsPanelInnerProps> = ({
|
||||
<SelectionFloatingPanel
|
||||
documentLookup={documentLookup}
|
||||
tags={tags}
|
||||
tagLookupById={tagLookupById}
|
||||
tagLookupById={props.tagLookupById}
|
||||
correspondents={correspondents}
|
||||
onBulkTagAdd={onBulkTagAdd}
|
||||
onBulkTagRemove={onBulkTagRemove}
|
||||
@@ -226,7 +161,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
||||
), [
|
||||
documentLookup,
|
||||
tags,
|
||||
tagLookupById,
|
||||
props.tagLookupById,
|
||||
correspondents,
|
||||
onBulkTagAdd,
|
||||
onBulkTagRemove,
|
||||
@@ -240,14 +175,13 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
||||
]);
|
||||
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<DocumentsPanelInnerProps> = ({
|
||||
return trail[trail.length - 1]?.id || 'root';
|
||||
}, [breadcrumbs, showingSearchResults]);
|
||||
|
||||
const selectionContextRef = useRef(null);
|
||||
// Context marker logic for clearing selection on nav
|
||||
const selectionContextRef = useRef<any>(null);
|
||||
useEffect(() => {
|
||||
const nextContext = showingSearchResults
|
||||
? { type: 'search', marker: searchResultIds }
|
||||
@@ -283,16 +218,16 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
||||
}, [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<DocumentsPanelInnerProps> = ({
|
||||
return list;
|
||||
}, [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 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<DocumentsPanelInnerProps> = ({
|
||||
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<DocumentsPanelInnerProps> = ({
|
||||
}, [shouldHandlePanelInteractions, clearSelection]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentsPanelHeader
|
||||
header={headerConfig}
|
||||
onBreadcrumbClick={onBreadcrumbNavigate}
|
||||
/>
|
||||
<div className="documents-panel-wrapper">
|
||||
<section
|
||||
ref={scrollRef}
|
||||
className={`documents-panel documents-panel--view-${panelVariant}`}
|
||||
onClick={handleSectionClick}
|
||||
>
|
||||
{renderBody()}
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
<DocumentsAssetContext.Provider value={assetContextValue}>
|
||||
<DocumentsViewStateContext.Provider value={viewStateContextValue}>
|
||||
<DocumentsCommandContext.Provider value={commandContextValue}>
|
||||
<DocumentsPanelHeader
|
||||
header={headerConfig}
|
||||
onBreadcrumbClick={onBreadcrumbNavigate}
|
||||
/>
|
||||
<div className="documents-panel-wrapper">
|
||||
<section
|
||||
ref={scrollRef}
|
||||
className={`documents-panel documents-panel--view-${panelVariant}`}
|
||||
onClick={handleSectionClick}
|
||||
>
|
||||
{renderBody()}
|
||||
</section>
|
||||
</div>
|
||||
</DocumentsCommandContext.Provider>
|
||||
</DocumentsViewStateContext.Provider>
|
||||
</DocumentsAssetContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user