refactor(documents): encapsulate panel logic and state into dedicated React contexts
This commit is contained in:
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,38 +50,10 @@ 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) => {
|
||||||
|
const {
|
||||||
headerLeading = null,
|
headerLeading = null,
|
||||||
onBreadcrumbNavigate,
|
onBreadcrumbNavigate,
|
||||||
currentFolderName,
|
currentFolderName,
|
||||||
@@ -91,26 +61,6 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
subfolders,
|
subfolders,
|
||||||
documents,
|
documents,
|
||||||
searchResultIds,
|
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 = () => { },
|
onRefresh = () => { },
|
||||||
sortField,
|
sortField,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
@@ -127,30 +77,33 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
onBulkReanalyze,
|
onBulkReanalyze,
|
||||||
folderOptions,
|
folderOptions,
|
||||||
onMoveDocumentsToFolder,
|
onMoveDocumentsToFolder,
|
||||||
searchQuery = '',
|
viewMode = 'list',
|
||||||
activeTagFilters = [],
|
onViewModeChange,
|
||||||
activeCorrespondentFilters = [],
|
} = props;
|
||||||
selectedFolder = null,
|
|
||||||
onDocumentTagAttach,
|
const {
|
||||||
onDocumentTagDetach,
|
assetContextValue,
|
||||||
}): ReactNode => {
|
viewStateContextValue,
|
||||||
|
commandContextValue,
|
||||||
|
scrollRef,
|
||||||
|
hasDocumentEntries,
|
||||||
|
} = useDocumentsContextValues(props);
|
||||||
|
|
||||||
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,7 +293,9 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
}, [shouldHandlePanelInteractions, clearSelection]);
|
}, [shouldHandlePanelInteractions, clearSelection]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DocumentsAssetContext.Provider value={assetContextValue}>
|
||||||
|
<DocumentsViewStateContext.Provider value={viewStateContextValue}>
|
||||||
|
<DocumentsCommandContext.Provider value={commandContextValue}>
|
||||||
<DocumentsPanelHeader
|
<DocumentsPanelHeader
|
||||||
header={headerConfig}
|
header={headerConfig}
|
||||||
onBreadcrumbClick={onBreadcrumbNavigate}
|
onBreadcrumbClick={onBreadcrumbNavigate}
|
||||||
@@ -503,7 +309,9 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
{renderBody()}
|
{renderBody()}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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).
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user