refactor(documents): encapsulate panel logic and state into dedicated React contexts
This commit is contained in:
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user