From 3f7c294948b2fee71183ef481dd571a1708fb99a Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Thu, 4 Dec 2025 01:32:28 +0100 Subject: [PATCH] feat: Remove `documentLink` prop and `onNavigatorSnapshot` logic, and add `openDetailPanel` and `closeDetailPanel` to document shell. --- frontend/src/app/DocumentsRoute.tsx | 89 ++++++++++++------- frontend/src/app/PanelManagerContext.tsx | 35 ++++++-- frontend/src/app/useDocumentsShell.ts | 8 +- frontend/src/app/useWorkspaceSurface.tsx | 4 - frontend/src/desktop/DesktopDocumentCard.tsx | 26 ++++-- frontend/src/desktop/DesktopPreviewCard.tsx | 46 +--------- frontend/src/desktop/DesktopWorkspace.tsx | 44 +-------- frontend/src/preview/DocumentViewerLayout.tsx | 41 ++++----- frontend/src/preview/DocumentViewerPanel.tsx | 19 ---- 9 files changed, 129 insertions(+), 183 deletions(-) diff --git a/frontend/src/app/DocumentsRoute.tsx b/frontend/src/app/DocumentsRoute.tsx index a4f60bc..694e91f 100644 --- a/frontend/src/app/DocumentsRoute.tsx +++ b/frontend/src/app/DocumentsRoute.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useMemo } from 'react'; -import type { ReactNode } from 'react'; +import type { ReactNode, ComponentProps } from 'react'; import { useNavigate } from 'react-router-dom'; import { DocumentsFilterProvider, @@ -13,25 +13,24 @@ import { PanelManagerProvider, usePanelManager } from './PanelManagerContext'; import Sidebar from '../sidebar/Sidebar'; import useDocumentsShell from './useDocumentsShell'; -const DocumentsRouteContent: React.FC = () => { - const { - sidebarProps, - surfaceConfig, - documentsFilter, - } = useDocumentsShell(); - const navigate = useNavigate(); +const DocumentsInner: React.FC<{ + sidebarProps: ComponentProps | null; + surfaceConfig: any; + onNavigate: (documentId: string) => void; +}> = ({ sidebarProps, surfaceConfig, onNavigate }) => { + const { openPreview } = usePreviewContext(); const { collapsed: sidebarCollapsed } = useSidebarContext(); const { sidebarSuppressed, expandSidebar, - openDetailPanel, } = usePanelManager(); - const { openPreview } = usePreviewContext(); - + const { openDetailPanel } = surfaceConfig; const sidebarHidden = sidebarCollapsed || sidebarSuppressed; - const handleHeaderBreadcrumbClick = useCallback((crumb: DocumentsHeaderBreadcrumb) => { + const navigate = useNavigate(); + + const handleBreadcrumbNavigate = useCallback((crumb: DocumentsHeaderBreadcrumb) => { if (!crumb || !crumb.id) { return; } @@ -39,10 +38,6 @@ const DocumentsRouteContent: React.FC = () => { navigate(target); }, [navigate]); - const handleDocumentNavigate = useCallback((documentId: string) => { - navigate(`/documents/${documentId}`); - }, [navigate]); - const handleOpenSidepanel = useCallback((docId: string) => { if (openDetailPanel) { openDetailPanel({ documentIds: [docId] }); @@ -51,9 +46,9 @@ const DocumentsRouteContent: React.FC = () => { const documentsTablePropsWithNav = useMemo(() => ( surfaceConfig.documentsTableProps - ? { ...surfaceConfig.documentsTableProps, onBreadcrumbNavigate: handleHeaderBreadcrumbClick } + ? { ...surfaceConfig.documentsTableProps, onBreadcrumbNavigate: handleBreadcrumbNavigate } : null - ), [surfaceConfig.documentsTableProps, handleHeaderBreadcrumbClick]); + ), [surfaceConfig.documentsTableProps, handleBreadcrumbNavigate]); const { surface } = useWorkspaceSurface({ sidebarHidden, @@ -87,30 +82,58 @@ const DocumentsRouteContent: React.FC = () => { ); }; - const content = renderSurface(); + return ( + + {renderSurface()} + + ); +}; + +const DocumentsRouteContent: React.FC = () => { + const { + sidebarProps, + surfaceConfig, + documentsFilter, + } = useDocumentsShell(); + const navigate = useNavigate(); + + const handleDocumentNavigate = useCallback((documentId: string) => { + navigate(`/documents/${documentId}`); + }, [navigate]); + return ( - - {content} - + ); }; -const DocumentsRoute: React.FC = () => ( - - - - - -); +const DocumentsRoute: React.FC = () => { + const { surfaceConfig } = useDocumentsShell(); + const { detailPanelOpen, closeDetailPanel } = surfaceConfig; + + return ( + + + + + + ); +}; export default DocumentsRoute; diff --git a/frontend/src/app/PanelManagerContext.tsx b/frontend/src/app/PanelManagerContext.tsx index feca22d..7627752 100644 --- a/frontend/src/app/PanelManagerContext.tsx +++ b/frontend/src/app/PanelManagerContext.tsx @@ -89,9 +89,11 @@ const applyPanelWidthToRoot = (panel: PanelKey, width: number, active: boolean): interface PanelManagerProviderProps { children: ReactNode; + isOpen?: boolean; + onClose?: () => void; } -export const PanelManagerProvider: React.FC = ({ children }) => { +export const PanelManagerProvider: React.FC = ({ children, isOpen, onClose }) => { const { collapsed, setCollapsed } = useSidebarContext(); const initialSidebarWidth = readStoredWidth('sidebar', DEFAULT_SIDEBAR_WIDTH); const initialDetailWidth = readStoredWidth('detail', DEFAULT_DETAIL_WIDTH); @@ -100,7 +102,13 @@ export const PanelManagerProvider: React.FC = ({ chil const [detailWidth, setDetailWidthState] = useState(() => clampPanelWidth('detail', initialDetailWidth)); const [resizingPanel, setResizingPanel] = useState(null); const [sidebarSuppressed, setSidebarSuppressed] = useState(false); - const [detailPanelOpen, setDetailPanelOpen] = useState(false); + const [detailPanelOpen, setDetailPanelOpen] = useState(Boolean(isOpen)); + + useEffect(() => { + if (isOpen !== undefined) { + setDetailPanelOpen(isOpen); + } + }, [isOpen]); const detailCloseHandlerRef = useRef(null); const panelWidthsRef = useRef({ sidebar: sidebarWidth, detail: detailWidth }); @@ -109,7 +117,11 @@ export const PanelManagerProvider: React.FC = ({ chil const closeDetailPanel = useCallback(() => { const handler = detailCloseHandlerRef.current; handler?.(); - }, []); + onClose?.(); + if (isOpen === undefined) { + setDetailPanelOpen(false); + } + }, [onClose, isOpen]); useEffect(() => { panelWidthsRef.current.sidebar = sidebarWidth; @@ -228,11 +240,16 @@ export const PanelManagerProvider: React.FC = ({ chil }, []); const setDetailActive = useCallback( - (isOpen) => { - setDetailPanelOpen(Boolean(isOpen)); - handlePanelLayoutChange('detail', isOpen ? 'opened' : 'closed'); + (active) => { + if (isOpen === undefined) { + setDetailPanelOpen(Boolean(active)); + } + if (!active) { + onClose?.(); + } + handlePanelLayoutChange('detail', active ? 'opened' : 'closed'); }, - [handlePanelLayoutChange], + [handlePanelLayoutChange, isOpen, onClose], ); const expandSidebar = useCallback(() => { @@ -394,8 +411,8 @@ export const usePanelResizeBindings = ( const handleProps = enabled ? { - onPointerDown: handlePointerDown, - } + onPointerDown: handlePointerDown, + } : {}; return { diff --git a/frontend/src/app/useDocumentsShell.ts b/frontend/src/app/useDocumentsShell.ts index e440620..d1b0fe0 100644 --- a/frontend/src/app/useDocumentsShell.ts +++ b/frontend/src/app/useDocumentsShell.ts @@ -6,7 +6,10 @@ import type Sidebar from '../sidebar/Sidebar'; import type { UseWorkspaceSurfaceArgs } from './useWorkspaceSurface'; import type { Identifier } from '../types/identifiers'; -type WorkspaceSurfaceConfig = Omit; +type WorkspaceSurfaceConfig = Omit & { + openDetailPanel?: (args: { documentIds?: Identifier[] }) => void; + closeDetailPanel?: () => void; +}; interface DocumentsShellView { sidebarProps: ComponentProps | null; @@ -23,8 +26,9 @@ const useDocumentsShell = (): DocumentsShellView => { documentsTableProps: (shell.documentsTableProps ?? null) as WorkspaceSurfaceConfig['documentsTableProps'], detailPanelProps: (shell.detailPanelProps ?? null) as WorkspaceSurfaceConfig['detailPanelProps'], detailPanelOpen: Boolean(shell.detailPanelOpen), + openDetailPanel: shell.openDetailPanel as WorkspaceSurfaceConfig['openDetailPanel'], + closeDetailPanel: shell.closeDetailPanel as WorkspaceSurfaceConfig['closeDetailPanel'], previewWorkspaceDocument: shell.previewWorkspaceDocument, - documentLink: shell.documentLink, previewDocumentId: (shell.previewDocumentId as Identifier | null | undefined) ?? null, closeDocumentPreview: shell.closeDocumentPreview as WorkspaceSurfaceConfig['closeDocumentPreview'], ensurePreviewData: shell.ensurePreviewData as WorkspaceSurfaceConfig['ensurePreviewData'], diff --git a/frontend/src/app/useWorkspaceSurface.tsx b/frontend/src/app/useWorkspaceSurface.tsx index 64f22a3..93b2ebe 100644 --- a/frontend/src/app/useWorkspaceSurface.tsx +++ b/frontend/src/app/useWorkspaceSurface.tsx @@ -36,7 +36,6 @@ export interface UseWorkspaceSurfaceArgs { detailPanelProps?: DetailPanelProps; detailPanelOpen?: boolean; previewWorkspaceDocument?: unknown; - documentLink?: unknown; previewDocumentId?: Identifier | null; ensureAssetUrl?: EnsureAssetUrl; ensurePreviewData?: EnsurePreviewData; @@ -56,7 +55,6 @@ export const useWorkspaceSurface = ({ detailPanelProps, detailPanelOpen = false, previewWorkspaceDocument, - documentLink, previewDocumentId, ensureAssetUrl, ensurePreviewData, @@ -181,7 +179,6 @@ export const useWorkspaceSurface = ({ const viewer = ( { if (!event) return; @@ -21,16 +22,18 @@ interface DesktopDocumentCardProps { docTagTokens?: string; ensureAssetUrl?: (...args: any[]) => Promise; getDocumentAsset?: (...args: any[]) => unknown; - handleNavigatorSnapshot?: (...args: any[]) => void; - cardPointerHandlers?: React.HTMLAttributes; onDocumentActivate?: (id: string, event?: any) => void; + onSelect: (ids: string[], extend?: boolean) => void; + onDeselect: (ids: string[]) => void; + selection: string[]; + requestCanvasFocus?: () => void; onTagDragEnter?: (event: React.DragEvent, docId: DocumentId) => void; onTagDragOver?: (event: React.DragEvent, docId: DocumentId) => void; onTagDragLeave?: (event: React.DragEvent, docId: DocumentId) => void; onTagDrop?: (event: React.DragEvent, doc: Document) => void; onDocTagDragStart?: (event: React.DragEvent, doc: Document, tag: any) => void; onDocTagDragEnd?: (event: React.DragEvent) => void; - layoutCard?: LayoutCard; + layoutCard: LayoutCard; } const DesktopDocumentCard: React.FC = ({ @@ -42,9 +45,11 @@ const DesktopDocumentCard: React.FC = ({ docTagTokens, ensureAssetUrl, getDocumentAsset, - handleNavigatorSnapshot, - cardPointerHandlers, onDocumentActivate, + onSelect, + onDeselect, + selection, + requestCanvasFocus, onTagDragEnter, onTagDragOver, onTagDragLeave, @@ -53,6 +58,16 @@ const DesktopDocumentCard: React.FC = ({ onDocTagDragEnd, layoutCard, }) => { + const cardPointerHandlers = useCardPointer( + layoutCard, + !!selected, + selection, + onSelect, + onDeselect, + onDocumentActivate, + requestCanvasFocus + ); + const correspondents = useMemo(() => resolveCorrespondents(doc), [doc]); const tags = Array.isArray(doc?.tags) ? doc.tags : []; @@ -108,7 +123,6 @@ const DesktopDocumentCard: React.FC = ({ title={doc.title} ensureAssetUrl={ensureAssetUrl} getDocumentAsset={getDocumentAsset} - onNavigatorSnapshot={handleNavigatorSnapshot} shouldLoad={shouldLoad} /> {correspondents.length > 0 && ( diff --git a/frontend/src/desktop/DesktopPreviewCard.tsx b/frontend/src/desktop/DesktopPreviewCard.tsx index 27be170..fa78a57 100644 --- a/frontend/src/desktop/DesktopPreviewCard.tsx +++ b/frontend/src/desktop/DesktopPreviewCard.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from 'react'; +import { useMemo } from 'react'; import type { JSX } from 'react'; import { resolveDocumentAssetUrl } from '../asset_manager'; import type { Identifier } from '../types/identifiers'; @@ -14,19 +14,11 @@ type EnsureAssetUrl = ( type GetDocumentAsset = (document: Document | null, assetType: string) => Asset | null; -interface NavigatorSnapshot { - url: string | null; - alt?: string; - width: number | null; - height: number | null; -} - interface DesktopPreviewCardProps { doc: Document | null; title?: string; ensureAssetUrl?: EnsureAssetUrl | null; getDocumentAsset?: GetDocumentAsset; - onNavigatorSnapshot?: (docId: Identifier, snapshot: NavigatorSnapshot | null) => void; shouldLoad?: boolean; } @@ -35,7 +27,6 @@ const DesktopPreviewCard = ({ title, ensureAssetUrl, getDocumentAsset, - onNavigatorSnapshot, shouldLoad = true, }: DesktopPreviewCardProps): JSX.Element => { const currentUrl = useMemo(() => { @@ -46,41 +37,6 @@ const DesktopPreviewCard = ({ }); }, [doc, ensureAssetUrl, getDocumentAsset, shouldLoad]); - const currentMetadata = useMemo(() => { - if (!doc || !getDocumentAsset) return null; - const asset = getDocumentAsset(doc, 'thumbnail'); - return asset?.metadata || null; - }, [doc, getDocumentAsset]); - const docId = doc?.id ?? null; - - const metadataWidth = Number((currentMetadata as { width?: number } | null)?.width); - const metadataHeight = Number((currentMetadata as { height?: number } | null)?.height); - - useEffect(() => { - if (!onNavigatorSnapshot || !docId) { - return undefined; - } - if (!currentUrl) { - onNavigatorSnapshot(docId, null); - return undefined; - } - const snapshot = { - url: currentUrl, - alt: title, - width: Number.isFinite(metadataWidth) && metadataWidth > 0 ? metadataWidth : null, - height: Number.isFinite(metadataHeight) && metadataHeight > 0 ? metadataHeight : null, - }; - onNavigatorSnapshot(docId, snapshot); - return () => onNavigatorSnapshot(docId, null); - }, [ - docId, - currentUrl, - title, - metadataWidth, - metadataHeight, - onNavigatorSnapshot, - ]); - const hasPreview = Boolean(currentUrl); const cardClasses = ['desk-item__card']; if (!hasPreview) cardClasses.push('desk-item__card--empty'); diff --git a/frontend/src/desktop/DesktopWorkspace.tsx b/frontend/src/desktop/DesktopWorkspace.tsx index 57c7793..ebf0389 100644 --- a/frontend/src/desktop/DesktopWorkspace.tsx +++ b/frontend/src/desktop/DesktopWorkspace.tsx @@ -9,7 +9,6 @@ import { LayoutStore, LayoutCard } from './LayoutSystem'; import DesktopDocumentCard from './DesktopDocumentCard'; import usePreviewMetadata from './hooks/usePreviewMetadata'; import useDeskTagInteractions from './tags/useDeskTagInteractions'; -import { useCardPointer } from './useCardPointer'; import '../styles/workspace/workspace-layout.css'; import '../styles/workspace/workspace-items.css'; import '../styles/workspace/workspace-cards.css'; @@ -21,17 +20,6 @@ import type { DocumentsListEntry, Document } from '../types/documents'; import { useAppState } from '../app/appState'; import { useDocumentOpen } from '../contexts/DocumentOpenContext'; -type TagLike = { id?: Identifier | null; label?: string; color?: string | null } | null; -type OverlaySource = { url: string; alt?: string | null; mimeType?: string | null; }; - -export interface DeskDocument { - id?: Identifier | null; - title?: string; - tags?: TagLike[] | null; - documentLink?: OverlaySource | null; - [key: string]: unknown; -} - interface DocumentSizeInfo { width: number; height: number; @@ -39,7 +27,7 @@ interface DocumentSizeInfo { } // Fallback size computation -const computeFallbackCardSize = (_doc: DeskDocument, defaultSize: number = 200): DocumentSizeInfo => { +const computeFallbackCardSize = (_doc: Document, defaultSize: number = 200): DocumentSizeInfo => { const size = Math.round(defaultSize * (1 / Math.SQRT2)); return { width: size, height: size, source: 'fallback' }; }; @@ -55,29 +43,6 @@ export interface DesktopWorkspaceProps { defaultCardSize?: number; } -// Wrapper to handle hooks per card -const DesktopDocumentContainer: React.FC & { - onSelect: (ids: string[], extend?: boolean) => void; - onDeselect: (ids: string[]) => void; - onDocumentActivate?: (id: string, event?: any) => void; - selection: string[]; - requestCanvasFocus?: () => void; -}> = React.memo((props) => { - const { layoutCard, selected, onSelect, onDeselect, onDocumentActivate, selection, requestCanvasFocus } = props; - - // We assume layoutCard is always present in this context - const cardPointerHandlers = useCardPointer(layoutCard!, !!selected, selection, onSelect, onDeselect, onDocumentActivate, requestCanvasFocus); - - return ( - - ); -}); - -DesktopDocumentContainer.displayName = 'DesktopDocumentContainer'; - const DesktopWorkspaceContent: React.FC = ({ entries, ensureAssetUrl, @@ -102,7 +67,7 @@ const DesktopWorkspaceContent: React.FC = ({ .filter((entry): entry is { type: 'document'; document: Document } & DocumentsListEntry => entry.type === 'document' && !!entry.document ) - .map(entry => entry.document as DeskDocument); + .map(entry => entry.document); }, [entries]); // Layout System Initialization @@ -189,7 +154,7 @@ const DesktopWorkspaceContent: React.FC = ({ const metadataMap = usePreviewMetadata(items, getDocumentAsset, ensureAssetUrl); - const ensureDocumentSize = useCallback((doc: DeskDocument): DocumentSizeInfo => { + const ensureDocumentSize = useCallback((doc: Document): DocumentSizeInfo => { if (doc.id) { const meta = metadataMap.get(String(doc.id)); if (meta && meta.width && meta.height) @@ -404,7 +369,7 @@ const DesktopWorkspaceContent: React.FC = ({ }); return ( - = ({ docTagTokens="" ensureAssetUrl={ensureAssetUrl} getDocumentAsset={getDocumentAsset} - handleNavigatorSnapshot={() => { }} onDocumentActivate={(_id, event) => { const isPreview = event && ((event as any).altKey || (event as any).button === 1); openDocument(doc, isPreview ? 'preview' : 'sidepanel'); diff --git a/frontend/src/preview/DocumentViewerLayout.tsx b/frontend/src/preview/DocumentViewerLayout.tsx index 7b30ce3..872565a 100644 --- a/frontend/src/preview/DocumentViewerLayout.tsx +++ b/frontend/src/preview/DocumentViewerLayout.tsx @@ -6,12 +6,6 @@ import MediaViewer from './MediaViewer'; import type { Document } from '../types/documents'; -interface DocumentLink { - url?: string; - mimeType?: string; - filename?: string; -} - interface ContentTabConfig { id?: string; label?: string; @@ -29,7 +23,6 @@ type LayoutMode = 'split' | 'stacked' | (string & {}); interface DocumentViewerLayoutProps { document?: Document | null; - documentLink?: DocumentLink | null; summaryProps?: Record; metadataPayload?: unknown; contentTabConfig?: ContentTabConfig | null; @@ -43,7 +36,6 @@ interface DocumentViewerLayoutProps { const DocumentViewerLayout = ({ document, - documentLink, summaryProps = {}, metadataPayload, contentTabConfig, @@ -58,28 +50,27 @@ const DocumentViewerLayout = ({ const viewportRef = useRef(null); const previewContent = useMemo(() => { - if (!document || !documentLink?.url) { + if (!document) { return null; } - const normalizedMimeType = (documentLink.mimeType - || document.mime_type - || '') - .toLowerCase(); - const normalizedFilename = documentLink.filename - || document.filename - || document.original_name - || ''; + const downloadUrl = document.current_version?.download?.url; + if (!downloadUrl) { + return null; + } + + const normalizedMimeType = (document.mime_type || '').toLowerCase(); + const normalizedFilename = document.filename; + const isPdf = normalizedMimeType === 'application/pdf' || normalizedMimeType === 'application/x-pdf'; if (isPdf) { - const documentTitle = document.title - || document.filename - || document.original_name; + const documentTitle = document.title; + return ( @@ -88,23 +79,23 @@ const DocumentViewerLayout = ({ return ( ); - }, [document, documentLink, viewportRef]); + }, [document, viewportRef]); const renderViewportPane = useCallback(() => (
- {!documentLink?.url ? ( + {!document?.current_version?.download?.url ? (
{previewLoadingMessage}
) : ( previewContent )}
- ), [previewContent, documentLink?.url, previewLoadingMessage, viewportRef]); + ), [previewContent, document?.current_version?.download?.url, previewLoadingMessage, viewportRef]); const viewportPane = renderViewportPane(); diff --git a/frontend/src/preview/DocumentViewerPanel.tsx b/frontend/src/preview/DocumentViewerPanel.tsx index ba76a14..e9366b8 100644 --- a/frontend/src/preview/DocumentViewerPanel.tsx +++ b/frontend/src/preview/DocumentViewerPanel.tsx @@ -230,24 +230,6 @@ const DocumentViewerPanel: React.FC = ({ const panelRef = useRef(null); const isStackedLayout = useViewerLayoutMode(panelRef, document?.id); - const resolvedDocumentLink = useMemo(() => { - if (!document) { - return null; - } - const downloadUrl = document.current_version?.download?.url; - const href = downloadUrl; - if (!href) { - return null; - } - const mimeType = document.mime_type; - const filename = document.current_version?.filename || document.filename || document.title || null; - return { - url: href, - mimeType, - filename, - }; - }, [document]); - const { panelStyle: managedDetailPanelStyle, handleProps: managedResizeHandleProps, @@ -407,7 +389,6 @@ const DocumentViewerPanel: React.FC = ({