feat: Refactor DocumentsPanel to use a shared application shell context for state management

This commit is contained in:
2025-12-12 02:57:10 +01:00
parent 34e6c3037f
commit 6ba8084caa
7 changed files with 109 additions and 338 deletions
@@ -2,7 +2,7 @@ import React from 'react';
import type { useWorkspaceSelection } from './useWorkspaceSelection'; import type { useWorkspaceSelection } from './useWorkspaceSelection';
import { createSafeContext } from '../utils/createSafeContext'; import { createSafeContext } from '../utils/createSafeContext';
export type WorkspaceSelectionValue = ReturnType<typeof useWorkspaceSelection>; type WorkspaceSelectionValue = ReturnType<typeof useWorkspaceSelection>;
const [WorkspaceSelectionContext, useWorkspaceSelectionContext] = createSafeContext<WorkspaceSelectionValue>('WorkspaceSelection'); const [WorkspaceSelectionContext, useWorkspaceSelectionContext] = createSafeContext<WorkspaceSelectionValue>('WorkspaceSelection');
+2 -5
View File
@@ -4,8 +4,6 @@ import type { DocumentsFilterValue } from '../documents/context/DocumentsFilterC
import type { UseWorkspaceSurfaceArgs } from './useWorkspaceSurface'; import type { UseWorkspaceSurfaceArgs } from './useWorkspaceSurface';
import type { Identifier } from '../types/identifiers'; import type { Identifier } from '../types/identifiers';
import useDocumentsPanelProps from '../documents/logic/useDocumentsPanelProps';
type WorkspaceSurfaceConfig = Omit<UseWorkspaceSurfaceArgs, 'sidebarHidden' | 'onExpandSidebar'> & { type WorkspaceSurfaceConfig = Omit<UseWorkspaceSurfaceArgs, 'sidebarHidden' | 'onExpandSidebar'> & {
openDetailPanel?: (documentId: Identifier) => void; openDetailPanel?: (documentId: Identifier) => void;
closeDetailPanel?: () => void; closeDetailPanel?: () => void;
@@ -21,11 +19,10 @@ interface DocumentsShellView {
const useDocumentsShell = (): DocumentsShellView => { const useDocumentsShell = (): DocumentsShellView => {
const shell = useAppShell() as any; const shell = useAppShell() as any;
const documentsPanelProps = useDocumentsPanelProps(shell as any);
return useMemo(() => { return useMemo(() => {
const surfaceConfig: WorkspaceSurfaceConfig = { const surfaceConfig: WorkspaceSurfaceConfig = {
documentsTableProps: documentsPanelProps, documentsTableProps: {},
detailPanelProps: (shell.detailPanel?.detailPanelProps ?? null) as WorkspaceSurfaceConfig['detailPanelProps'], detailPanelProps: (shell.detailPanel?.detailPanelProps ?? null) as WorkspaceSurfaceConfig['detailPanelProps'],
detailPanelOpen: Boolean(shell.detailPanel?.detailPanelOpen), detailPanelOpen: Boolean(shell.detailPanel?.detailPanelOpen),
openDetailPanel: shell.detailPanel?.openDetailPanel as WorkspaceSurfaceConfig['openDetailPanel'], openDetailPanel: shell.detailPanel?.openDetailPanel as WorkspaceSurfaceConfig['openDetailPanel'],
@@ -46,7 +43,7 @@ const useDocumentsShell = (): DocumentsShellView => {
documentsManager: shell.managers?.documentsManager, documentsManager: shell.managers?.documentsManager,
foldersManager: shell.folderTree?.foldersManager, foldersManager: shell.folderTree?.foldersManager,
}; };
}, [shell, documentsPanelProps]); }, [shell]);
}; };
export default useDocumentsShell; export default useDocumentsShell;
-1
View File
@@ -150,7 +150,6 @@ export const useWorkspaceSurface = ({
return { return {
content: ( content: (
<DocumentsPanel <DocumentsPanel
{...documentsTableProps}
headerLeading={sidebarToggle} headerLeading={sidebarToggle}
/> />
), ),
@@ -1,192 +0,0 @@
import type { WorkspaceSelectionValue } from '../../app/WorkspaceSelectionContext';
import type { Identifier } from '../../types/identifiers';
interface Breadcrumb {
id?: Identifier;
name?: string;
label?: string;
title?: string;
}
interface FolderClickHandlers {
onDrop?: (...args: unknown[]) => void;
onDragOver?: (...args: unknown[]) => void;
onDragLeave?: (...args: unknown[]) => void;
}
interface UseDocumentsPanelPropsArgs {
currentFolderName?: string | null;
breadcrumbs?: Breadcrumb[];
refreshCurrentFolder?: () => void | Promise<void>;
currentSubfolders?: unknown[];
documents?: unknown[];
searchQuery?: string;
searchResultIds?: Identifier[] | null;
folderClickHandlers: FolderClickHandlers;
selectedFolder?: Identifier | 'root' | null;
selectFolder?: (...args: unknown[]) => void;
handleFolderDragStart?: (...args: unknown[]) => void;
handleFolderDragEnd?: (...args: unknown[]) => void;
draggedFolderId?: Identifier | null;
handleFolderRename?: (...args: unknown[]) => void;
handleDocumentTitleUpdate?: (...args: unknown[]) => void;
focusedRowKey?: Identifier | string | null;
draggedDocumentIds?: Identifier[];
handleDocumentDragStart?: (...args: unknown[]) => void;
handleDocumentDragEnd?: (...args: unknown[]) => void;
searchLoading?: boolean;
tagLookupById?: unknown;
activeTagFilters?: Identifier[];
activeCorrespondentFilters?: Identifier[];
ensureAssetUrl?: (...args: unknown[]) => void;
getDocumentAsset?: (...args: unknown[]) => unknown;
handleDocumentTagAttach?: (docId: Identifier, tagId: Identifier) => void;
handleDocumentTagDetach?: (docId: Identifier, tagId: Identifier) => void;
documentsViewMode?: string;
documentsSortField?: string;
documentsSortDirection?: string;
handleDocumentsSortFieldChange?: (field: string) => void;
handleDocumentsSortDirectionToggle?: () => void;
handleDocumentsViewModeChange?: (mode: string) => void;
clearDocumentSelection?: () => void;
handleDeleteSelection?: () => void;
handleEntryPointerCore?: (...args: unknown[]) => void;
tags?: unknown[];
correspondents?: unknown[];
correspondentLookupById?: unknown;
handleBulkTagAddFromDetail?: (...args: unknown[]) => void;
handleBulkTagRemoveFromDetail?: (...args: unknown[]) => void;
handleBulkCorrespondentAdd?: (...args: unknown[]) => void;
handleBulkCorrespondentRemove?: (...args: unknown[]) => void;
handleBulkSelectionReanalyze?: (...args: unknown[]) => void;
folderOptions?: unknown[];
moveDocumentsToFolder?: (...args: unknown[]) => void;
documentLookup?: unknown;
selectionValue: WorkspaceSelectionValue;
}
const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => {
const {
folderTree: {
currentFolderName,
breadcrumbs,
currentSubfolders,
folderClickHandlers,
selectedFolder,
selectFolder,
handleFolderDragStart,
handleFolderDragEnd,
draggedFolderId,
handleFolderRename,
folderOptions,
moveDocumentsToFolder,
} = {},
search: {
documents,
searchQuery,
searchResultIds,
searchLoading,
documentsViewMode,
documentsSortField,
documentsSortDirection,
handleDocumentsSortFieldChange,
handleDocumentsSortDirectionToggle,
handleDocumentsViewModeChange,
} = {},
ui: {
refreshCurrentFolder,
} = {},
mutations: {
handleDocumentTitleUpdate,
draggedDocumentIds,
handleDocumentDragStart,
handleDocumentDragEnd,
} = {},
selection: {
selectionValue,
handleDeleteSelection,
handleEntryPointerCore,
handleBulkSelectionReanalyze,
} = {},
tags: {
tagLookupById,
activeTagFilters,
tags,
handleDocumentTagAttach,
handleDocumentTagDetach,
handleBulkTagAddFromDetail,
handleBulkTagRemoveFromDetail,
} = {},
correspondents: {
activeCorrespondentFilters,
correspondents,
correspondentLookupById,
handleBulkCorrespondentAdd,
handleBulkCorrespondentRemove,
} = {},
preview: {
ensureAssetUrl,
getDocumentAsset,
} = {},
managers: {
documentLookup,
} = {},
} = props as any;
const focusedRowKey = selectionValue?.focusedEntryKey || null;
return {
currentFolderName,
breadcrumbs,
onRefresh: refreshCurrentFolder,
subfolders: currentSubfolders,
documents,
searchQuery,
searchResultIds,
selectedFolder,
onFolderSelect: selectFolder,
onFolderDrop: folderClickHandlers.onDrop,
onFolderDragOver: folderClickHandlers.onDragOver,
onFolderDragLeave: folderClickHandlers.onDragLeave,
onFolderDragStart: handleFolderDragStart,
onFolderDragEnd: handleFolderDragEnd,
draggedFolderId,
onFolderRename: handleFolderRename,
onDocumentRename: handleDocumentTitleUpdate,
focusedRowKey,
draggingDocumentIds: draggedDocumentIds,
onDocumentDragStart: handleDocumentDragStart,
onDocumentDragEnd: handleDocumentDragEnd,
isSearchLoading: searchLoading,
tagLookupById,
activeTagFilters,
activeCorrespondentFilters,
activeCorrespondentIds: activeCorrespondentFilters,
ensureAssetUrl,
getDocumentAsset,
onDocumentTagAttach: handleDocumentTagAttach,
onDocumentTagDetach: handleDocumentTagDetach,
viewMode: documentsViewMode,
sortField: documentsSortField,
sortDirection: documentsSortDirection,
onSortFieldChange: handleDocumentsSortFieldChange,
onSortDirectionToggle: handleDocumentsSortDirectionToggle,
onViewModeChange: handleDocumentsViewModeChange,
onDeleteSelection: handleDeleteSelection,
onEntryPointer: handleEntryPointerCore,
tags,
correspondents,
correspondentLookupById,
documentLookup,
onBulkTagAdd: handleBulkTagAddFromDetail,
onBulkTagRemove: handleBulkTagRemoveFromDetail,
onBulkCorrespondentAdd: handleBulkCorrespondentAdd,
onBulkCorrespondentRemove: handleBulkCorrespondentRemove,
onBulkReanalyze: handleBulkSelectionReanalyze,
folderOptions,
onMoveDocumentsToFolder: moveDocumentsToFolder,
selectionValue,
};
};
export default useDocumentsPanelProps;
+46 -63
View File
@@ -1,27 +1,29 @@
import React, { import React, {
useCallback,
useMemo, useMemo,
useState, useState,
useRef, useRef,
useEffect, useEffect,
useCallback,
} from 'react'; } from 'react';
import TagRemovalZone from '../components/TagRemovalZone'; import TagRemovalZone from '../components/TagRemovalZone';
import { DocumentsList, DocumentsGrid } from '../DocumentsView'; import { DocumentsList, DocumentsGrid } from '../DocumentsView';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { import type {
DocumentsListEntry, DocumentsListEntry,
Correspondent,
} from '../../types/documents'; } from '../../types/documents';
import type { Identifier } from '../../types/identifiers';
const EntryType = {
folder: 'folder' as const,
document: 'document' as const,
};
import DesktopWorkspace from '../../desktop/components/DesktopWorkspace'; import DesktopWorkspace from '../../desktop/components/DesktopWorkspace';
import { import {
WorkspaceSelectionProvider, WorkspaceSelectionProvider,
useWorkspaceSelectionContext, useWorkspaceSelectionContext,
type WorkspaceSelectionValue,
} from '../../app/WorkspaceSelectionContext'; } from '../../app/WorkspaceSelectionContext';
import DocumentsPanelHeader, { import DocumentsPanelHeader, {
DocumentsPanelHeaderConfig, DocumentsPanelHeaderConfig,
DocumentsHeaderBreadcrumb,
} from './DocumentsPanelHeader'; } from './DocumentsPanelHeader';
import { SelectionFloatingPanel } from '../features/selection/SelectionFloatingActions'; import { SelectionFloatingPanel } from '../features/selection/SelectionFloatingActions';
import { createDocumentsTableHeaderActions } from './DocumentsToolbar'; import { createDocumentsTableHeaderActions } from './DocumentsToolbar';
@@ -35,58 +37,31 @@ import { DocumentsAssetContext } from '../context/DocumentsAssetContext';
import { DocumentsViewStateContext } from '../context/DocumentsViewStateContext'; import { DocumentsViewStateContext } from '../context/DocumentsViewStateContext';
import { DocumentsCommandContext } from '../context/DocumentsCommandContext'; import { DocumentsCommandContext } from '../context/DocumentsCommandContext';
import { useDocumentsContextValues } from './useDocumentsContextValues'; import { useDocumentsContextValues } from './useDocumentsContextValues';
import { useAppShell } from '../../lib/context/AppShellContext';
const EntryType = {
folder: 'folder' as const,
document: 'document' as const,
};
export interface DocumentsPanelInnerProps {
headerLeading?: ReactNode;
onBreadcrumbNavigate?: (crumb: DocumentsHeaderBreadcrumb) => void;
[key: string]: any;
}
interface DocumentsPanelProps extends DocumentsPanelInnerProps {
selectionValue: WorkspaceSelectionValue;
correspondentLookupById?: Map<Identifier, Correspondent>;
}
import type { TagInteractionHandlers } from '../interactions/useTagInteractions'; import type { TagInteractionHandlers } from '../interactions/useTagInteractions';
export interface DocumentsViewProps { export interface DocumentsViewProps {
entries: DocumentsListEntry[]; entries: DocumentsListEntry[];
tagHandlers?: TagInteractionHandlers; tagHandlers?: TagInteractionHandlers;
[key: string]: any;
} }
const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = (props) => { interface DocumentsPanelProps {
headerLeading?: ReactNode;
}
const DocumentsPanelInner: React.FC<DocumentsPanelProps> = (props) => {
const { headerLeading } = props;
const shell = useAppShell();
const { const {
headerLeading = null, search: { documents, searchResultIds, documentsViewMode: viewMode, documentsSortField: sortField, documentsSortDirection: sortDirection, handleDocumentsViewModeChange: onViewModeChange, handleDocumentsSortFieldChange: onSortFieldChange, handleDocumentsSortDirectionToggle: onSortDirectionToggle },
onBreadcrumbNavigate, folderTree: { currentFolderName, breadcrumbs, currentSubfolders: subfolders, folderOptions, moveDocumentsToFolder: onMoveDocumentsToFolder, refreshCurrentFolder: onRefresh, handleBreadcrumbNavigate: onBreadcrumbNavigate },
currentFolderName, selection: { handleDeleteSelection: onDeleteSelection },
breadcrumbs, managers: { documentLookup },
subfolders, tags: { tags, handleBulkTagAddFromDetail: onBulkTagAdd, handleBulkTagRemoveFromDetail: onBulkTagRemove },
documents, correspondents: { correspondents, handleBulkCorrespondentAdd: onBulkCorrespondentAdd, handleBulkCorrespondentRemove: onBulkCorrespondentRemove },
searchResultIds, mutations: { handleBulkSelectionReanalyze: onBulkReanalyze },
onRefresh = () => { }, } = shell as any;
sortField,
sortDirection,
onSortFieldChange,
onSortDirectionToggle,
onDeleteSelection,
documentLookup,
tags,
correspondents,
onBulkTagAdd,
onBulkTagRemove,
onBulkCorrespondentAdd,
onBulkCorrespondentRemove,
onBulkReanalyze,
folderOptions,
onMoveDocumentsToFolder,
viewMode = 'list',
onViewModeChange,
} = props;
const { const {
assetContextValue, assetContextValue,
@@ -95,7 +70,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = (props) => {
tagHandlers, tagHandlers,
scrollRef, scrollRef,
hasDocumentEntries, hasDocumentEntries,
} = useDocumentsContextValues(props); } = useDocumentsContextValues();
const { const {
clearSelection, clearSelection,
@@ -119,7 +94,6 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = (props) => {
const showingSearchResults = Array.isArray(searchResultIds); const showingSearchResults = Array.isArray(searchResultIds);
const rows = showingSearchResults && searchDocuments ? searchDocuments : documents; const rows = showingSearchResults && searchDocuments ? searchDocuments : documents;
const headerTitle = showingSearchResults const headerTitle = showingSearchResults
? 'Search results' ? 'Search results'
: currentFolderName || 'Documents'; : currentFolderName || 'Documents';
@@ -150,13 +124,16 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = (props) => {
toggleIncludeDescendants, toggleIncludeDescendants,
], ],
); );
const { tagLookupById, correspondentLookupById } = (shell as any).tags;
const floatingActions = useMemo(() => ( const floatingActions = useMemo(() => (
<SelectionFloatingPanel <SelectionFloatingPanel
documentLookup={documentLookup} documentLookup={documentLookup}
tags={tags} tags={tags}
tagLookupById={props.tagLookupById} tagLookupById={tagLookupById}
correspondents={correspondents} correspondents={correspondents}
correspondentLookupById={props.correspondentLookupById} correspondentLookupById={correspondentLookupById}
onBulkTagAdd={onBulkTagAdd} onBulkTagAdd={onBulkTagAdd}
onBulkTagRemove={onBulkTagRemove} onBulkTagRemove={onBulkTagRemove}
onBulkCorrespondentAdd={onBulkCorrespondentAdd} onBulkCorrespondentAdd={onBulkCorrespondentAdd}
@@ -170,9 +147,9 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = (props) => {
), [ ), [
documentLookup, documentLookup,
tags, tags,
props.tagLookupById, tagLookupById,
correspondents, correspondents,
props.correspondentLookupById, correspondentLookupById,
onBulkTagAdd, onBulkTagAdd,
onBulkTagRemove, onBulkTagRemove,
onBulkCorrespondentAdd, onBulkCorrespondentAdd,
@@ -183,6 +160,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = (props) => {
onMoveDocumentsToFolder, onMoveDocumentsToFolder,
clearSelection, clearSelection,
]); ]);
const headerConfig: DocumentsPanelHeaderConfig = useMemo(() => ({ const headerConfig: DocumentsPanelHeaderConfig = useMemo(() => ({
title: headerTitle, title: headerTitle,
subtitle: null, subtitle: null,
@@ -230,14 +208,14 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = (props) => {
const entries = useMemo(() => { const entries = useMemo(() => {
const list: DocumentsListEntry[] = []; // Explicit type const list: DocumentsListEntry[] = []; // Explicit type
if (!showingSearchResults) { if (!showingSearchResults) {
subfolders.forEach((folder: any) => { (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: any) => { (rows || []).forEach((doc: any) => {
if (!doc || !doc.id) { if (!doc || !doc.id) {
return; return;
} }
@@ -261,7 +239,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = (props) => {
desk: DEFAULT_DESKTOP_CARD_SIZE, desk: DEFAULT_DESKTOP_CARD_SIZE,
}); });
const isSearchLoading = props.isSearchLoading || false; const isSearchLoading = (shell as any).search?.isSearchLoading || false;
const renderBody = () => { const renderBody = () => {
const hasEntries = entries.length > 0; const hasEntries = entries.length > 0;
@@ -328,10 +306,15 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = (props) => {
); );
}; };
const DocumentsPanel: React.FC<DocumentsPanelProps> = ({ selectionValue, ...rest }) => ( const DocumentsPanel: React.FC<DocumentsPanelProps> = (props) => {
<WorkspaceSelectionProvider value={selectionValue}> const shell = useAppShell();
<DocumentsPanelInner {...rest} /> const selectionValue = (shell as any).selection?.selectionValue;
</WorkspaceSelectionProvider>
); return (
<WorkspaceSelectionProvider value={selectionValue}>
<DocumentsPanelInner {...props} />
</WorkspaceSelectionProvider>
);
};
export default DocumentsPanel; export default DocumentsPanel;
@@ -4,7 +4,7 @@ import PanelHeader from '../../components/PanelHeader';
import BreadcrumbTrail from '../../components/BreadcrumbTrail'; import BreadcrumbTrail from '../../components/BreadcrumbTrail';
import type { Identifier } from '../../types/identifiers'; import type { Identifier } from '../../types/identifiers';
export interface DocumentsHeaderBreadcrumb { interface DocumentsHeaderBreadcrumb {
id?: Identifier; id?: Identifier;
name?: string; name?: string;
label?: string; label?: string;
@@ -1,34 +1,28 @@
import { useMemo, useCallback, useRef, useEffect } from 'react'; import { useMemo, useCallback, useRef, useEffect } from 'react';
import type { DocumentsPanelInnerProps } from './DocumentsPanel';
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../features/selection/useEntryPointer'; import { isPointerModifierEvent, isPrimaryPointerEvent } from '../features/selection/useEntryPointer';
import { useDocumentsFilter } from '../context/DocumentsFilterContext'; import { useDocumentsFilter } from '../context/DocumentsFilterContext';
import { useWorkspaceSelectionContext } from '../../app/WorkspaceSelectionContext'; import { useWorkspaceSelectionContext } from '../../app/WorkspaceSelectionContext';
import type { Identifier } from '../../types/identifiers';
import { useStatusToast } from '../../lib/context/StatusToastContext'; import { useStatusToast } from '../../lib/context/StatusToastContext';
import { useTagInteractions } from '../interactions/useTagInteractions'; import { useTagInteractions } from '../interactions/useTagInteractions';
import { subscribeToToast } from '../features/tagging/tagTransfer'; import { subscribeToToast } from '../features/tagging/tagTransfer';
import { useAppShell } from '../../lib/context/AppShellContext';
const EntryType = { const EntryType = {
folder: 'folder', folder: 'folder',
document: 'document', document: 'document',
}; };
export const useDocumentsContextValues = (props: DocumentsPanelInnerProps) => { export const useDocumentsContextValues = () => {
const shell = useAppShell();
const { const {
ensureAssetUrl, preview: { ensureAssetUrl, getDocumentAsset },
getDocumentAsset, search: { isSearchLoading, searchQuery, activeTagFilters, activeCorrespondentFilters, searchResultIds, documents },
isSearchLoading, folderTree: { selectedFolder },
searchQuery = '', mutations: { handleDocumentDragStart, handleDocumentDragEnd, draggedDocumentIds: draggingDocumentIds, handleDocumentTagAttach, handleDocumentTagDetach },
activeTagFilters = [], selection: { handleEntryPointerCore: onEntryPointer },
activeCorrespondentFilters = [], correspondents: { activeCorrespondentIds, correspondentLookupById },
selectedFolder = null, tags: { tagLookupById },
onDocumentDragStart, } = shell as any;
onDocumentDragEnd,
onEntryPointer,
activeCorrespondentIds = [],
draggingDocumentIds = [],
tagLookupById,
} = props;
const { const {
setFocusedEntryKey, setFocusedEntryKey,
@@ -53,8 +47,8 @@ export const useDocumentsContextValues = (props: DocumentsPanelInnerProps) => {
// Handlers // Handlers
const tagHandlers = useTagInteractions({ const tagHandlers = useTagInteractions({
onAssignTagToDocument: props.onDocumentTagAttach, onAssignTagToDocument: handleDocumentTagAttach,
onRemoveTagFromDocument: props.onDocumentTagDetach, onRemoveTagFromDocument: handleDocumentTagDetach,
onTagClick: toggleTagFilter, onTagClick: toggleTagFilter,
}); });
@@ -67,14 +61,14 @@ export const useDocumentsContextValues = (props: DocumentsPanelInnerProps) => {
() => new Set(activeCorrespondentIds || []), () => new Set(activeCorrespondentIds || []),
[activeCorrespondentIds], [activeCorrespondentIds],
); );
const showingSearchResults = Array.isArray(props.searchResultIds); const showingSearchResults = Array.isArray(searchResultIds);
const hasDocumentEntries = (props.documents || []).length > 0 || (showingSearchResults && (props.searchResultIds || []).length > 0); const hasDocumentEntries = (documents || []).length > 0 || (showingSearchResults && (searchResultIds || []).length > 0);
const viewId = useMemo(() => { const viewId = useMemo(() => {
if (showingSearchResults) { if (showingSearchResults) {
const trimmedQuery = searchQuery.trim(); const trimmedQuery = (searchQuery || '').trim();
const tagsKey = [...activeTagFilters].sort().join(','); const tagsKey = [...(activeTagFilters || [])].sort().join(',');
const correspondentsKey = [...activeCorrespondentFilters].sort().join(','); const correspondentsKey = [...(activeCorrespondentFilters || [])].sort().join(',');
return `search:${trimmedQuery}|tags:${tagsKey}|corr:${correspondentsKey}`; return `search:${trimmedQuery}|tags:${tagsKey}|corr:${correspondentsKey}`;
} }
const folderKey = selectedFolder && selectedFolder !== '' ? selectedFolder : 'root'; const folderKey = selectedFolder && selectedFolder !== '' ? selectedFolder : 'root';
@@ -115,19 +109,19 @@ export const useDocumentsContextValues = (props: DocumentsPanelInnerProps) => {
const handleDocumentDragStartLocal = useCallback( const handleDocumentDragStartLocal = useCallback(
(event: any, doc: any) => { (event: any, doc: any) => {
suppressDocumentClickRef.current = true; suppressDocumentClickRef.current = true;
onDocumentDragStart?.(event, doc); handleDocumentDragStart?.(event, doc);
}, },
[onDocumentDragStart], [handleDocumentDragStart],
); );
const handleDocumentDragEndLocal = useCallback( const handleDocumentDragEndLocal = useCallback(
(event: any) => { (event: any) => {
onDocumentDragEnd?.(event); handleDocumentDragEnd?.(event);
requestAnimationFrame(() => { requestAnimationFrame(() => {
suppressDocumentClickRef.current = false; suppressDocumentClickRef.current = false;
}); });
}, },
[onDocumentDragEnd], [handleDocumentDragEnd],
); );
// Context Values Construction // Context Values Construction
@@ -136,71 +130,61 @@ export const useDocumentsContextValues = (props: DocumentsPanelInnerProps) => {
getDocumentAsset, getDocumentAsset,
}), [ensureAssetUrl, getDocumentAsset]); }), [ensureAssetUrl, getDocumentAsset]);
const draggedFolderId = (shell as any).folderTree?.draggedFolderId;
const viewStateContextValue = useMemo(() => ({ const viewStateContextValue = useMemo(() => ({
viewId, viewId,
scrollRef, scrollRef,
tagLookupById, tagLookupById,
correspondentLookupById: props.correspondentLookupById, correspondentLookupById,
activeCorrespondentIdSet, activeCorrespondentIdSet,
draggingDocumentIdsSet, draggingDocumentIdsSet,
draggedFolderId: props.draggedFolderId, draggedFolderId,
}), [ }), [
viewId, viewId,
scrollRef, scrollRef,
tagLookupById, tagLookupById,
activeCorrespondentIdSet, activeCorrespondentIdSet,
draggingDocumentIdsSet, draggingDocumentIdsSet,
props.draggedFolderId, draggedFolderId,
props.correspondentLookupById, correspondentLookupById,
]); ]);
// Use refs to stabilize handlers and avoid massive dependency arrays const commandContextValue = useMemo(() => {
const latestPropsRef = useRef(props); const anyShell = shell as any;
const latestHandlersRef = useRef({ return {
folder: {
onClick: handleFolderClick,
onSelect: anyShell.folderTree?.selectFolder,
onRename: anyShell.folderTree?.handleFolderRename,
onDrag: {
start: anyShell.folderTree?.handleFolderDragStart,
end: anyShell.folderTree?.handleFolderDragEnd,
over: anyShell.folderTree?.folderClickHandlers?.onDragOver,
leave: anyShell.folderTree?.folderClickHandlers?.onDragLeave,
drop: anyShell.folderTree?.folderClickHandlers?.onDrop,
},
},
document: {
onRename: anyShell.mutations?.handleDocumentTitleUpdate,
onDrag: {
start: handleDocumentDragStartLocal,
end: handleDocumentDragEndLocal,
},
},
correspondents: {
onClick: toggleCorrespondentFilter,
},
onEntryPointer,
}
}, [
handleFolderClick, handleFolderClick,
shell,
handleDocumentDragStartLocal, handleDocumentDragStartLocal,
handleDocumentDragEndLocal, handleDocumentDragEndLocal,
tagHandlers,
toggleTagFilter,
toggleCorrespondentFilter, toggleCorrespondentFilter,
}); onEntryPointer,
]);
// Update refs on every render
latestPropsRef.current = props;
latestHandlersRef.current = {
handleFolderClick,
handleDocumentDragStartLocal,
handleDocumentDragEndLocal,
tagHandlers,
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),
},
},
correspondents: {
onClick: (c: any) => latestHandlersRef.current.toggleCorrespondentFilter(c),
},
onEntryPointer: (entry: any, e: any) => latestPropsRef.current.onEntryPointer?.(entry, e),
}), []);
return { return {
assetContextValue, assetContextValue,