refactor: Consolidate document view props into a shared interface and refine tag drop payload handling.

This commit is contained in:
2025-11-25 12:11:42 +01:00
parent 9c116cfe8c
commit 5191d50d42
4 changed files with 105 additions and 163 deletions
+46 -22
View File
@@ -8,7 +8,7 @@ import React, {
useSyncExternalStore,
} from 'react';
import { resolveDocumentAssetUrl } from '../asset_manager';
import type { EnsureAssetUrl, GetAsset } from '../asset_manager';
import type { GetAsset } from '../asset_manager';
import { formatTransform } from '../utils/math';
import useDocumentDrag from './useDocumentDrag';
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
@@ -118,18 +118,11 @@ type WorkspaceSnapshotState = {
initialLoadDone: boolean;
};
interface DesktopWorkspaceProps {
entries?: DeskDocument[];
onDocumentActivate?: (...args: unknown[]) => void;
onDocumentClick?: (...args: unknown[]) => void;
onDocumentTagDrop?: (...args: unknown[]) => void;
ensureAssetUrl?: EnsureAssetUrl;
getDocumentAsset?: GetAsset;
activeTagFilters?: Array<Identifier | null>;
tenantId?: Identifier | null;
viewId?: string | null;
documentLinks?: Map<Identifier, unknown> | null;
ensureDownloadUrl?: (docId: Identifier, options?: { force?: boolean }) => Promise<unknown>;
import type { DocumentsViewProps } from '../documents/panel/DocumentsPanel';
interface DesktopWorkspaceProps extends DocumentsViewProps {
// Override entries if needed, or rely on DocumentsViewProps
// entries: DocumentsListEntry[];
}
interface DesktopWorkspaceViewProps {
@@ -209,7 +202,22 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
promoteSelectionOrder,
} = useWorkspaceSelectionContext();
const items = useMemo<DeskDocument[]>(
() => (Array.isArray(entries) ? entries.filter((doc): doc is DeskDocument => Boolean(doc)) : []),
() => {
if (!Array.isArray(entries)) return [];
return entries.flatMap((entry: any) => {
if (!entry) return [];
// Handle DocumentsListEntry
if ('type' in entry && entry.type === 'document' && entry.document) {
return [entry.document as DeskDocument];
}
if ('type' in entry && entry.type === 'folder') {
return [];
}
return [];
});
},
[entries],
);
@@ -938,6 +946,26 @@ function DesktopWorkspaceView({
markLayoutDirty,
dragTransformsRef,
}: DesktopWorkspaceViewProps) {
const handleDeskDocumentActivate = useCallback(
(docId: Identifier) => {
const doc = documentLookup.get(String(docId));
if (doc && onDocumentActivate) {
onDocumentActivate(doc, undefined as any);
}
},
[documentLookup, onDocumentActivate],
);
const handleDeskDocumentClick = useCallback(
(docId: Identifier, event: any) => {
const doc = documentLookup.get(String(docId));
if (doc && onDocumentClick) {
onDocumentClick(doc, event);
}
},
[documentLookup, onDocumentClick],
);
const { handlePointerDown, handlePointerMove, handlePointerUp, handlePointerCancel } =
useDocumentDrag({
engine,
@@ -954,7 +982,7 @@ function DesktopWorkspaceView({
recalcVisibleDocIds,
settings: dragSettings,
containerRef,
onDocumentActivate,
onDocumentActivate: handleDeskDocumentActivate,
selectedDocumentIds,
markLayoutDirty,
}) as {
@@ -974,17 +1002,13 @@ function DesktopWorkspaceView({
handlePointerMove,
handlePointerUp,
handlePointerCancel,
onDocumentClick,
onDocumentClick: handleDeskDocumentClick,
onDocumentStackSelect,
onPromoteSelection,
onDocumentActivate,
onDocumentActivate: handleDeskDocumentActivate,
selectedDocumentIds,
openOverlayForDoc,
}) as {
getCardPointerHandlers: (doc: DeskDocument) => React.HTMLAttributes<HTMLDivElement>;
handleShellKeyDown: React.KeyboardEventHandler<HTMLDivElement>;
focusShell: () => void;
};
});
useEffect(() => {
focusShell();
+16 -74
View File
@@ -1,86 +1,21 @@
import React, { useMemo } from 'react';
import type { DragEvent, MouseEvent, RefObject } from 'react';
import { FolderIcon, CheckIcon, CloseIcon } from '../ui/icons';
import DocumentThumbnailImage from './DocumentThumbnailImage';
import CorrespondentLinks from './CorrespondentLinks';
import { getTagColorStyle } from '../utils/colors';
import { resolveCorrespondents } from './correspondents';
import { writeTagTransferData } from './tagTransfer';
import { writeTagTransferData, parseTagTransferPayload } from './tagTransfer';
import useInlineRename from './useInlineRename';
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
import type { Identifier } from '../types/identifiers';
export interface FolderLike {
id?: Identifier | 'root';
name?: string;
}
import type {
FolderLike,
DocumentLike,
} from './DocumentsList';
import type { DocumentsViewProps } from './panel/DocumentsPanel';
export interface DocumentTag {
id?: Identifier;
label?: string;
color?: string | null;
}
export interface DocumentCorrespondent {
id?: Identifier;
name?: string;
count?: number;
}
export interface DocumentLike {
id?: Identifier;
title?: string;
tags?: DocumentTag[] | null;
correspondents?: DocumentCorrespondent[] | null;
}
export type FolderEntry = {
type: 'folder';
id: Identifier | 'root';
key: string;
folder: FolderLike;
};
export type DocumentEntry = {
type: 'document';
id: Identifier;
key: string;
document: DocumentLike;
};
export type DocumentsGridEntry = FolderEntry | DocumentEntry;
type FolderEventHandler = (folder: FolderLike, event: MouseEvent<HTMLDivElement>) => void;
type DocumentEventHandler = (document: DocumentLike, event: MouseEvent<HTMLDivElement>) => void;
interface DocumentsGridProps {
entries: DocumentsGridEntry[];
draggingDocumentIdsSet?: Set<Identifier> | null;
draggedFolderId?: Identifier | 'root' | null;
onFolderClick?: FolderEventHandler;
onFolderSelect?: (folderId: Identifier | 'root') => void;
onFolderDragOver?: (event: DragEvent<HTMLDivElement>, folderId: Identifier | 'root') => void;
onFolderDragLeave?: (event: DragEvent<HTMLDivElement>) => void;
onFolderDrop?: (event: DragEvent<HTMLDivElement>, folderId: Identifier | 'root') => void;
onFolderDragStart?: (event: DragEvent<HTMLDivElement>, folderId: Identifier | 'root') => void;
onFolderDragEnd?: (event: DragEvent<HTMLDivElement>) => void;
onFolderRename?: (folderId: Identifier | 'root', nextName: string) => Promise<boolean> | boolean;
onDocumentClick?: DocumentEventHandler;
onDocumentActivate?: DocumentEventHandler;
onDocumentDragStart?: (event: DragEvent<HTMLDivElement>, document: DocumentLike) => void;
onDocumentDragEnd?: (event: DragEvent<HTMLDivElement>) => void;
onDocumentTagDragOver?: (event: DragEvent<HTMLDivElement>) => void;
onDocumentTagDragLeave?: (event: DragEvent<HTMLDivElement>) => void;
onDocumentTagDrop?: (event: DragEvent<HTMLDivElement>, documentId: Identifier) => void;
ensureAssetUrl?: (...args: any[]) => unknown;
getDocumentAsset?: (...args: any[]) => unknown;
interface DocumentsGridProps extends DocumentsViewProps {
gridIconSize?: number;
tagLookupById?: Map<Identifier, DocumentTag> | null;
onTagClick?: (tagId: Identifier) => void;
scrollRef?: RefObject<HTMLElement | null>;
onCorrespondentClick?: (correspondentId: Identifier) => void;
activeCorrespondentIdSet?: Set<Identifier> | null;
onDocumentRename?: (docId: Identifier, title: string) => Promise<boolean> | boolean;
}
const DocumentsGrid: React.FC<DocumentsGridProps> = ({
@@ -332,8 +267,15 @@ const DocumentsGrid: React.FC<DocumentsGridProps> = ({
onDragOverCapture={(event) => onDocumentTagDragOver?.(event)}
onDragLeave={onDocumentTagDragLeave}
onDragLeaveCapture={onDocumentTagDragLeave}
onDrop={(event) => onDocumentTagDrop?.(event, doc.id)}
onDropCapture={(event) => onDocumentTagDrop?.(event, doc.id)}
onDrop={(event) => {
event.preventDefault();
event.stopPropagation();
const payload = parseTagTransferPayload(event);
if (payload && onDocumentTagDrop) {
onDocumentTagDrop(doc.id, payload);
}
}}
>
<DocumentThumbnailImage
document={doc}
+11 -4
View File
@@ -6,11 +6,11 @@ import { formatDate } from '../utils/date';
import DocumentThumbnailImage from './DocumentThumbnailImage';
import CorrespondentLinks from './CorrespondentLinks';
import { resolveCorrespondents } from './correspondents';
import { writeTagTransferData } from './tagTransfer';
import { writeTagTransferData, parseTagTransferPayload } from './tagTransfer';
import useInlineRename from './useInlineRename';
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
import type { Identifier } from '../types/identifiers';
import type { DocumentsListProps } from './panel/DocumentsPanel';
import type { DocumentsViewProps } from './panel/DocumentsPanel';
export interface FolderLike {
id?: Identifier | 'root';
@@ -58,7 +58,7 @@ export type DocumentsListEntry = FolderEntry | DocumentEntry;
export type FolderEventHandler = (folder: FolderLike, event: MouseEvent<HTMLTableRowElement>) => void;
export type DocumentEventHandler = (document: DocumentLike, event: MouseEvent<HTMLTableRowElement>) => void;
const DocumentsList: React.FC<DocumentsListProps> = ({
const DocumentsList: React.FC<DocumentsViewProps> = ({
entries,
draggingDocumentIdsSet,
draggedFolderId,
@@ -315,7 +315,14 @@ const DocumentsList: React.FC<DocumentsListProps> = ({
onDragEnd={(event) => onDocumentDragEnd?.(event)}
onDragOver={onDocumentTagDragOver}
onDragLeave={onDocumentTagDragLeave}
onDrop={(event) => onDocumentTagDrop?.(event, doc.id)}
onDrop={(event) => {
event.preventDefault();
event.stopPropagation();
const payload = parseTagTransferPayload(event);
if (payload && onDocumentTagDrop) {
onDocumentTagDrop(doc.id, payload);
}
}}
>
<td className="thumb-cell">
<DocumentThumbnailImage
+32 -63
View File
@@ -10,7 +10,7 @@ import type {
DocumentTag,
} from '../DocumentsList';
import DesktopWorkspace from '../../desktop/DesktopWorkspace';
import { isTagTransferEvent, parseTagTransferPayload } from '../tagTransfer';
import { isTagTransferEvent } from '../tagTransfer';
import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay';
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../useEntryPointer';
import {
@@ -48,7 +48,7 @@ const defaultGetDocumentAsset = (_doc?: unknown, _type?: string) => null;
export type DocumentLinkLike = { url?: string | null; mimeType?: string | null };
export interface DocumentsListProps {
export interface DocumentsViewProps {
entries: DocumentsListEntry[];
draggingDocumentIdsSet?: Set<Identifier> | null;
draggedFolderId?: Identifier | 'root' | null;
@@ -56,25 +56,33 @@ export interface DocumentsListProps {
getDocumentAsset?: (...args: any[]) => unknown;
onFolderClick?: FolderEventHandler;
onFolderSelect?: (folderId: Identifier | 'root') => void;
onFolderDragOver?: (event: DragEvent<HTMLTableRowElement>, folderId: Identifier | 'root') => void;
onFolderDragLeave?: (event: DragEvent<HTMLTableRowElement>) => void;
onFolderDrop?: (event: DragEvent<HTMLTableRowElement>, folderId: Identifier | 'root') => void;
onFolderDragStart?: (event: DragEvent<HTMLTableRowElement>, folderId: Identifier | 'root') => void;
onFolderDragEnd?: (event: DragEvent<HTMLTableRowElement>) => 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;
onDocumentClick?: DocumentEventHandler;
onDocumentActivate?: DocumentEventHandler;
onDocumentDragStart?: (event: DragEvent<HTMLTableRowElement>, document: DocumentLike) => void;
onDocumentDragEnd?: (event: DragEvent<HTMLTableRowElement>) => void;
onDocumentTagDragOver?: (event: DragEvent<HTMLTableRowElement>) => void;
onDocumentTagDragLeave?: (event: DragEvent<HTMLTableRowElement>) => void;
onDocumentTagDrop?: (event: DragEvent<HTMLTableRowElement>, documentId: Identifier) => void;
onDocumentDragStart?: (event: DragEvent<HTMLElement>, document: DocumentLike) => void;
onDocumentDragEnd?: (event: DragEvent<HTMLElement>) => void;
onDocumentTagDragOver?: (event: DragEvent<HTMLElement>) => void;
onDocumentTagDragLeave?: (event: DragEvent<HTMLElement>) => void;
onDocumentTagDrop?: (documentId: Identifier, tag: any) => 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)
tenantId?: Identifier | null;
viewId?: string | null;
documentLinks?: Map<Identifier, any> | null;
ensureDownloadUrl?: (docId: Identifier, options?: { force?: boolean }) => Promise<any>;
onDocumentStackSelect?: (docIds: Identifier[], event?: any) => void;
onPromoteSelection?: (docId: Identifier | null) => void;
activeTagFilters?: Array<Identifier | null>;
}
const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
@@ -777,53 +785,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
[entries],
);
const handleListDocumentTagDrop = useCallback(
(event: React.DragEvent<HTMLElement>, docId: Identifier) => {
event.preventDefault();
event.stopPropagation();
const payload = parseTagTransferPayload(event);
if (payload && onDocumentTagDrop) {
onDocumentTagDrop(docId, payload);
}
},
[onDocumentTagDrop],
);
const deskWorkspaceProps = useMemo(
() => ({
entries: rows,
onDocumentActivate,
onDocumentClick: onEntryPointer,
onDocumentStackSelect: handleDeskDocumentStackSelect,
onPromoteSelection: promoteSelectionOrder,
onDocumentTagDrop,
ensureAssetUrl,
getDocumentAsset,
activeTagFilters,
tenantId: currentTenantId,
viewId: deskViewId,
documentLinks: documentLinkMap,
ensureDownloadUrl,
}),
[
rows,
onDocumentActivate,
onEntryPointer,
handleDeskDocumentStackSelect,
promoteSelectionOrder,
onDocumentTagDrop,
ensureAssetUrl,
getDocumentAsset,
activeTagFilters,
currentTenantId,
deskViewId,
documentLinkMap,
ensureDownloadUrl,
],
);
const listProps: DocumentsListProps = {
const viewProps: DocumentsViewProps = {
entries,
draggingDocumentIdsSet: draggingSet,
draggedFolderId,
@@ -840,7 +802,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
onDocumentDragEnd: handleDocumentDragEndLocal,
onDocumentTagDragOver: handleDocumentTagDragOver,
onDocumentTagDragLeave: handleDocumentTagDragLeave,
onDocumentTagDrop: handleListDocumentTagDrop,
onDocumentTagDrop,
onDocumentRename,
onFolderRename,
ensureAssetUrl,
@@ -850,6 +812,13 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
onCorrespondentClick: toggleCorrespondentFilter,
activeCorrespondentIdSet: activeCorrespondentIdSet,
scrollRef,
// Desk specific
onDocumentStackSelect: handleDeskDocumentStackSelect,
onPromoteSelection: promoteSelectionOrder,
tenantId: currentTenantId,
viewId: deskViewId,
documentLinks: documentLinkMap,
ensureDownloadUrl,
};
const renderBody = () => {
@@ -874,12 +843,12 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
switch (viewMode) {
case 'desk':
return <DesktopWorkspace {...deskWorkspaceProps} />;
return <DesktopWorkspace {...viewProps} />;
case 'grid':
return <DocumentsGrid {...listProps} gridIconSize={gridIconSize} />;
return <DocumentsGrid {...viewProps} gridIconSize={gridIconSize} />;
case 'list':
default:
return <DocumentsList {...listProps} />;
return <DocumentsList {...viewProps} />;
}
};