refactor: centralize documents route configuration into new useDocumentsShell hook and export related types.

This commit is contained in:
2025-11-24 23:55:37 +01:00
parent 70e7bda87e
commit 9ef63a87cb
5 changed files with 88 additions and 92 deletions
+8 -79
View File
@@ -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<string, unknown>,
) => Promise<unknown> | void;
type EnsurePreviewData = (docId: Identifier, options?: Record<string, unknown>) => Promise<unknown>;
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<string, unknown> | null;
documentsTableProps?: DocumentsTableProps | null;
detailPanelProps?: Record<string, unknown> | 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<Record<string, unknown>>(
() => (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 ? <Sidebar {...sidebarPropsWithActions} /> : null;
const sidebarNode = !sidebarHidden && sidebarProps ? <Sidebar {...sidebarProps} /> : null;
const surfaceDetail = surface && (surface as { detail?: ReactNode }).detail ? (surface as { detail?: ReactNode }).detail : null;
const surfaceBody = surface ? surface.content : null;
+52
View File
@@ -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<UseWorkspaceSurfaceArgs, 'sidebarHidden' | 'onExpandSidebar'>;
interface DocumentsShellView {
sidebarProps: ComponentProps<typeof Sidebar> | null;
surfaceConfig: WorkspaceSurfaceConfig;
documentsFilter: DocumentsFilterValue;
}
const useDocumentsShell = (): DocumentsShellView => {
const shell = useAppShell();
return useMemo(() => {
const sidebar = (shell.sidebarProps as ComponentProps<typeof Sidebar> | 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;
+25 -10
View File
@@ -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<string, unknown>) => Promise<unknown> | void;
type EnsurePreviewData = (docId: Identifier, options?: Record<string, unknown>) => Promise<unknown>;
type GetDocumentAsset = (document: unknown, assetType: string) => unknown;
type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
export type EnsureAssetUrl = (
docId: Identifier,
asset: unknown,
options?: Record<string, unknown>,
) => Promise<unknown> | void;
export type EnsurePreviewData = (docId: Identifier, options?: Record<string, unknown>) => Promise<unknown>;
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<typeof DocumentsPanel> | null;
export type DetailPanelProps = (ComponentProps<typeof DocumentViewerPanel> & {
onClose?: () => void;
onOpenPreview?: (args: { documentIds: Array<string> }) => void;
folderNodes?: Map<Identifier | 'root', unknown>;
ensureFolderData?: (
folderId: Identifier | 'root',
options?: { force?: boolean; includeDocuments?: boolean },
) => Promise<void>;
}) | null;
interface UseWorkspaceSurfaceArgs {
export type WorkspaceSurface = { content: ReactNode; detail?: ReactNode | null } | null;
export interface UseWorkspaceSurfaceArgs {
sidebarHidden?: boolean;
onExpandSidebar?: () => void;
documentsTableProps?: Record<string, any> | null;
detailPanelProps?: (Record<string, any> & { 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;
}
@@ -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;
}
@@ -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;