diff --git a/frontend/src/app/DocumentsRoute.tsx b/frontend/src/app/DocumentsRoute.tsx index 0142b62..dbf9eaf 100644 --- a/frontend/src/app/DocumentsRoute.tsx +++ b/frontend/src/app/DocumentsRoute.tsx @@ -1,70 +1,22 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import type { ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useAppShell } from '../appShellContext'; import { DocumentsFilterProvider, } from '../documents/context/DocumentsFilterContext'; -import type { DocumentsFilterValue } from '../documents/context/DocumentsFilterContext'; import { useWorkspaceSurface } from './useWorkspaceSurface'; import { DocumentsHeaderBreadcrumb } from '../documents/panel/DocumentsPanelHeader'; import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext'; import { PanelManagerProvider, usePanelManager } from './PanelManagerContext'; import Sidebar from '../sidebar/Sidebar'; -import type { Identifier } from '../types/identifiers'; - -type EnsureAssetUrl = ( - docId: Identifier, - asset: unknown, - options?: Record, -) => Promise | void; - -type EnsurePreviewData = (docId: Identifier, options?: Record) => Promise; -type GetDocumentAsset = (document: unknown, assetType: string) => unknown; -type NotifyApiError = (error: unknown, fallbackMessage?: string) => void; - -interface DocumentsTableProps { - breadcrumbs?: DocumentsHeaderBreadcrumb[] | null; - onBreadcrumbNavigate?: (crumb: DocumentsHeaderBreadcrumb) => void; - [key: string]: unknown; -} - -interface DocumentsRouteAppShell { - sidebarProps?: Record | null; - documentsTableProps?: DocumentsTableProps | null; - detailPanelProps?: Record | null; - detailPanelOpen?: boolean; - openTagsModal?: () => void; - openCorrespondentsModal?: () => void; - previewWorkspaceDocument?: unknown; - documentLink?: unknown; - previewDocumentId?: Identifier | null; - closeDocumentPreview?: () => void; - ensurePreviewData?: EnsurePreviewData; - ensureAssetUrl?: EnsureAssetUrl; - getDocumentAsset?: GetDocumentAsset; - notifyApiError?: NotifyApiError; - documentsFilter: DocumentsFilterValue; -} +import useDocumentsShell from './useDocumentsShell'; const DocumentsRouteContent: React.FC = () => { const { sidebarProps, - documentsTableProps, - detailPanelProps, - detailPanelOpen, - openTagsModal, - openCorrespondentsModal, - previewWorkspaceDocument, - documentLink, - previewDocumentId, - closeDocumentPreview, - ensurePreviewData, - ensureAssetUrl, - getDocumentAsset, - notifyApiError, + surfaceConfig, documentsFilter, - } = useAppShell() as unknown as DocumentsRouteAppShell; + } = useDocumentsShell(); const navigate = useNavigate(); const { collapsed: sidebarCollapsed } = useSidebarContext(); const { @@ -72,20 +24,6 @@ const DocumentsRouteContent: React.FC = () => { expandSidebar, } = usePanelManager(); - const safeSidebarProps = useMemo>( - () => (sidebarProps && Object(sidebarProps) === sidebarProps ? sidebarProps : {}), - [sidebarProps], - ); - - const sidebarPropsWithActions = useMemo( - () => ({ - ...safeSidebarProps, - onManageTags: openTagsModal, - onManageCorrespondents: openCorrespondentsModal, - }), - [safeSidebarProps, openTagsModal, openCorrespondentsModal], - ); - const sidebarHidden = sidebarCollapsed || sidebarSuppressed; const handleHeaderBreadcrumbClick = useCallback((crumb: DocumentsHeaderBreadcrumb) => { @@ -97,25 +35,16 @@ const DocumentsRouteContent: React.FC = () => { }, [navigate]); const documentsTablePropsWithNav = useMemo(() => ( - documentsTableProps - ? { ...documentsTableProps, onBreadcrumbNavigate: handleHeaderBreadcrumbClick } + surfaceConfig.documentsTableProps + ? { ...surfaceConfig.documentsTableProps, onBreadcrumbNavigate: handleHeaderBreadcrumbClick } : null - ), [documentsTableProps, handleHeaderBreadcrumbClick]); + ), [surfaceConfig.documentsTableProps, handleHeaderBreadcrumbClick]); const { surface } = useWorkspaceSurface({ sidebarHidden, onExpandSidebar: expandSidebar, + ...surfaceConfig, documentsTableProps: documentsTablePropsWithNav, - detailPanelProps, - detailPanelOpen, - previewWorkspaceDocument, - documentLink, - previewDocumentId, - ensureAssetUrl, - ensurePreviewData, - getDocumentAsset, - notifyApiError, - closeDocumentPreview, }); useEffect(() => { @@ -127,7 +56,7 @@ const DocumentsRouteContent: React.FC = () => { const renderSurface = () => { const layoutClass = `documents-main${sidebarHidden ? ' documents-main--sidebar-hidden' : ''}`; - const sidebarNode = !sidebarHidden ? : null; + const sidebarNode = !sidebarHidden && sidebarProps ? : null; const surfaceDetail = surface && (surface as { detail?: ReactNode }).detail ? (surface as { detail?: ReactNode }).detail : null; const surfaceBody = surface ? surface.content : null; diff --git a/frontend/src/app/useDocumentsShell.ts b/frontend/src/app/useDocumentsShell.ts new file mode 100644 index 0000000..e440620 --- /dev/null +++ b/frontend/src/app/useDocumentsShell.ts @@ -0,0 +1,52 @@ +import { useMemo } from 'react'; +import type { ComponentProps } from 'react'; +import { useAppShell } from '../appShellContext'; +import type { DocumentsFilterValue } from '../documents/context/DocumentsFilterContext'; +import type Sidebar from '../sidebar/Sidebar'; +import type { UseWorkspaceSurfaceArgs } from './useWorkspaceSurface'; +import type { Identifier } from '../types/identifiers'; + +type WorkspaceSurfaceConfig = Omit; + +interface DocumentsShellView { + sidebarProps: ComponentProps | null; + surfaceConfig: WorkspaceSurfaceConfig; + documentsFilter: DocumentsFilterValue; +} + +const useDocumentsShell = (): DocumentsShellView => { + const shell = useAppShell(); + + return useMemo(() => { + const sidebar = (shell.sidebarProps as ComponentProps | undefined) || null; + const surfaceConfig: WorkspaceSurfaceConfig = { + documentsTableProps: (shell.documentsTableProps ?? null) as WorkspaceSurfaceConfig['documentsTableProps'], + detailPanelProps: (shell.detailPanelProps ?? null) as WorkspaceSurfaceConfig['detailPanelProps'], + detailPanelOpen: Boolean(shell.detailPanelOpen), + 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'], + ensureAssetUrl: shell.ensureAssetUrl as WorkspaceSurfaceConfig['ensureAssetUrl'], + getDocumentAsset: shell.getDocumentAsset as WorkspaceSurfaceConfig['getDocumentAsset'], + notifyApiError: shell.notifyApiError as WorkspaceSurfaceConfig['notifyApiError'], + }; + + const sidebarWithActions = sidebar + ? { + ...sidebar, + onManageTags: shell.openTagsModal as (() => void) | undefined, + onManageCorrespondents: shell.openCorrespondentsModal as (() => void) | undefined, + } + : null; + + return { + sidebarProps: sidebarWithActions, + surfaceConfig, + documentsFilter: shell.documentsFilter as DocumentsFilterValue, + }; + }, [shell]); +}; + +export default useDocumentsShell; diff --git a/frontend/src/app/useWorkspaceSurface.tsx b/frontend/src/app/useWorkspaceSurface.tsx index 4b5a819..d13f899 100644 --- a/frontend/src/app/useWorkspaceSurface.tsx +++ b/frontend/src/app/useWorkspaceSurface.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo } from 'react'; -import type { ReactNode } from 'react'; +import type { ComponentProps, ReactNode } from 'react'; import { SidebarExpandIcon } from '../ui/icons'; import DocumentsPanel from '../documents/panel/DocumentsPanel'; import DocumentViewerPanel from '../preview/DocumentViewerPanel'; @@ -7,18 +7,33 @@ import { usePanelManager } from './PanelManagerContext'; import { FolderManagerProvider } from '../folders/FolderManagerContext'; import type { Identifier } from '../types/identifiers'; -type EnsureAssetUrl = (docId: Identifier, asset: unknown, options?: Record) => Promise | void; -type EnsurePreviewData = (docId: Identifier, options?: Record) => Promise; -type GetDocumentAsset = (document: unknown, assetType: string) => unknown; -type NotifyApiError = (error: unknown, fallbackMessage?: string) => void; +export type EnsureAssetUrl = ( + docId: Identifier, + asset: unknown, + options?: Record, +) => Promise | void; +export type EnsurePreviewData = (docId: Identifier, options?: Record) => Promise; +export type GetDocumentAsset = (document: unknown, assetType: string) => unknown; +export type NotifyApiError = (error: unknown, fallbackMessage?: string) => void; -type WorkspaceSurface = { content: ReactNode; detail?: ReactNode | null } | null; +export type DocumentsTableProps = ComponentProps | null; +export type DetailPanelProps = (ComponentProps & { + onClose?: () => void; + onOpenPreview?: (args: { documentIds: Array }) => void; + folderNodes?: Map; + ensureFolderData?: ( + folderId: Identifier | 'root', + options?: { force?: boolean; includeDocuments?: boolean }, + ) => Promise; +}) | null; -interface UseWorkspaceSurfaceArgs { +export type WorkspaceSurface = { content: ReactNode; detail?: ReactNode | null } | null; + +export interface UseWorkspaceSurfaceArgs { sidebarHidden?: boolean; onExpandSidebar?: () => void; - documentsTableProps?: Record | null; - detailPanelProps?: (Record & { onClose?: () => void }) | null; + documentsTableProps?: DocumentsTableProps; + detailPanelProps?: DetailPanelProps; detailPanelOpen?: boolean; previewWorkspaceDocument?: unknown; documentLink?: unknown; @@ -30,7 +45,7 @@ interface UseWorkspaceSurfaceArgs { closeDocumentPreview?: () => void; } -interface UseWorkspaceSurfaceResult { +export interface UseWorkspaceSurfaceResult { surface: WorkspaceSurface; } diff --git a/frontend/src/documents/SelectionFloatingActions.tsx b/frontend/src/documents/SelectionFloatingActions.tsx index 321035a..ded7c66 100644 --- a/frontend/src/documents/SelectionFloatingActions.tsx +++ b/frontend/src/documents/SelectionFloatingActions.tsx @@ -99,7 +99,7 @@ const splitLabelSegments = (input: unknown): string[] => { const buildFolderTreeOptions = (tree?: FolderTreeNode[] | null): SelectionAssignmentMenuItem[] => { const entries: SelectionAssignmentMenuItem[] = []; - const traverse = (nodes: FolderTreeNode[] | undefined | null, parentSegments: string[]) => { + const traverse = (nodes: FolderTreeNode[] | null, parentSegments: string[]) => { if (!Array.isArray(nodes) || nodes.length === 0) { return; } diff --git a/frontend/src/hooks/documents/useWorkspaceBreadcrumbs.ts b/frontend/src/hooks/documents/useWorkspaceBreadcrumbs.ts index 060afdf..3033686 100644 --- a/frontend/src/hooks/documents/useWorkspaceBreadcrumbs.ts +++ b/frontend/src/hooks/documents/useWorkspaceBreadcrumbs.ts @@ -43,8 +43,8 @@ const useWorkspaceBreadcrumbs = ({ continue; } - let fallbackName: string | null | undefined = '…'; - let parentId: FolderId | null | undefined = null; + let fallbackName: string | null = '…'; + let parentId: FolderId | null = null; if (currentFolder && currentFolder.id === currentId) { fallbackName = currentFolder.name;