221 lines
6.8 KiB
TypeScript
221 lines
6.8 KiB
TypeScript
import React, { useCallback, useEffect, useMemo } from 'react';
|
|
import type { ReactNode } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useAppShell } from '../appShellContext';
|
|
import DocumentsLayout from './DocumentsLayout';
|
|
import { useWorkspaceSurface } from './useWorkspaceSurface';
|
|
import PanelHeader from '../ui/PanelHeader';
|
|
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
|
|
import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext';
|
|
import { PanelManagerProvider, usePanelManager } from './PanelManagerContext';
|
|
|
|
type Identifier = string | number;
|
|
|
|
type Breadcrumb = { id?: Identifier; name?: string; label?: string; title?: string };
|
|
|
|
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 ResolveApiPath = (path: string) => string;
|
|
type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
|
|
|
|
interface DocumentsTableProps {
|
|
breadcrumbs?: Breadcrumb[] | null;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface DocumentsRouteAppShell {
|
|
sidebarProps?: Record<string, unknown> | null;
|
|
documentsTableProps?: DocumentsTableProps | null;
|
|
detailPanelProps?: Record<string, unknown> | null;
|
|
detailPanelOpen?: boolean;
|
|
documentsViewMode?: string;
|
|
deskWorkspaceProps?: Record<string, unknown> | null;
|
|
openTagsModal?: () => void;
|
|
openCorrespondentsModal?: () => void;
|
|
previewWorkspaceDocument?: unknown;
|
|
previewWorkspaceEntry?: unknown;
|
|
previewDocumentId?: Identifier | null;
|
|
closeDocumentPreview?: () => void;
|
|
ensurePreviewData?: EnsurePreviewData;
|
|
resolveApiPath?: ResolveApiPath;
|
|
ensureAssetUrl?: EnsureAssetUrl;
|
|
getDocumentAsset?: GetDocumentAsset;
|
|
notifyApiError?: NotifyApiError;
|
|
}
|
|
|
|
const DocumentsRouteContent: React.FC = () => {
|
|
const {
|
|
sidebarProps,
|
|
documentsTableProps,
|
|
detailPanelProps,
|
|
detailPanelOpen,
|
|
documentsViewMode,
|
|
deskWorkspaceProps,
|
|
openTagsModal,
|
|
openCorrespondentsModal,
|
|
previewWorkspaceDocument,
|
|
previewWorkspaceEntry,
|
|
previewDocumentId,
|
|
closeDocumentPreview,
|
|
ensurePreviewData,
|
|
resolveApiPath,
|
|
ensureAssetUrl,
|
|
getDocumentAsset,
|
|
notifyApiError,
|
|
} = useAppShell() as DocumentsRouteAppShell;
|
|
const navigate = useNavigate();
|
|
const { collapsed: sidebarCollapsed } = useSidebarContext();
|
|
const {
|
|
sidebarSuppressed,
|
|
expandSidebar,
|
|
} = usePanelManager();
|
|
|
|
const safeSidebarProps = useMemo<Record<string, unknown>>(
|
|
() => (sidebarProps && typeof sidebarProps === 'object' ? sidebarProps : {}),
|
|
[sidebarProps],
|
|
);
|
|
|
|
const sidebarPropsWithActions = useMemo(
|
|
() => ({
|
|
...safeSidebarProps,
|
|
onManageTags: openTagsModal,
|
|
onManageCorrespondents: openCorrespondentsModal,
|
|
}),
|
|
[safeSidebarProps, openTagsModal, openCorrespondentsModal],
|
|
);
|
|
|
|
const sidebarHidden = sidebarCollapsed || sidebarSuppressed;
|
|
|
|
const handleHeaderBreadcrumbClick = useCallback((crumb: Breadcrumb) => {
|
|
if (!crumb || !crumb.id) {
|
|
return;
|
|
}
|
|
const target = crumb.id === 'root' ? '/documents' : `/documents/folder/${crumb.id}`;
|
|
navigate(target);
|
|
}, [navigate]);
|
|
|
|
const { surface } = useWorkspaceSurface({
|
|
sidebarHidden,
|
|
onExpandSidebar: expandSidebar,
|
|
documentsTableProps,
|
|
detailPanelProps,
|
|
detailPanelOpen,
|
|
viewMode: documentsViewMode,
|
|
deskWorkspaceProps,
|
|
previewWorkspaceDocument,
|
|
previewWorkspaceEntry,
|
|
previewDocumentId,
|
|
ensureAssetUrl,
|
|
ensurePreviewData,
|
|
getDocumentAsset,
|
|
resolveApiPath,
|
|
notifyApiError,
|
|
closeDocumentPreview,
|
|
});
|
|
|
|
useEffect(() => {
|
|
document.body.classList.add('has-main-content');
|
|
return () => {
|
|
document.body.classList.remove('has-main-content');
|
|
};
|
|
}, []);
|
|
|
|
if (!surface) {
|
|
return (
|
|
<DocumentsLayout sidebarProps={sidebarPropsWithActions}>
|
|
<div className="main-content main-content--documents">
|
|
<div className="main-content__body main-content__body--documents" />
|
|
</div>
|
|
</DocumentsLayout>
|
|
);
|
|
}
|
|
|
|
const variant = surface.variant || 'documents';
|
|
|
|
const surfaceDetail = (surface as { detail?: ReactNode }).detail || null;
|
|
const hasDetail = Boolean(surfaceDetail);
|
|
|
|
const mainContentClass = `main-content main-content--${variant}${
|
|
hasDetail ? ' main-content--has-detail' : ''
|
|
}`;
|
|
const bodyClass = `main-content__body main-content__body--${variant}${
|
|
hasDetail ? ' main-content__body--has-detail' : ''
|
|
}`;
|
|
|
|
const header = surface.header || null;
|
|
|
|
let headerTitle: React.ReactNode = null;
|
|
if (header) {
|
|
const breadcrumbEntries = Array.isArray(header.breadcrumbs) ? header.breadcrumbs.filter(Boolean) : [];
|
|
const lastIndex = breadcrumbEntries.length - 1;
|
|
const trailEntries = breadcrumbEntries.length
|
|
? breadcrumbEntries.map((crumb, index) => ({
|
|
id: crumb.id ?? index,
|
|
label: crumb.name ?? crumb.label ?? crumb.title ?? '',
|
|
onClick: index < lastIndex ? () => handleHeaderBreadcrumbClick(crumb) : null,
|
|
}))
|
|
: [{ id: 'current-location', label: header.title }];
|
|
|
|
headerTitle = (
|
|
<h2 className="main-content__title">
|
|
<BreadcrumbTrail
|
|
entries={trailEntries}
|
|
className="main-content__breadcrumbs"
|
|
separator="/"
|
|
/>
|
|
{header.subtitle ? (
|
|
<span className="main-content__subtitle">{header.subtitle}</span>
|
|
) : null}
|
|
</h2>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<DocumentsLayout sidebarProps={sidebarPropsWithActions}>
|
|
<div className={mainContentClass}>
|
|
{header ? (
|
|
<>
|
|
<div className="main-content__header-wrapper">
|
|
<PanelHeader
|
|
className="main-content__header"
|
|
leading={header.leading}
|
|
title={headerTitle}
|
|
titleTag="h2"
|
|
actions={header.actions}
|
|
/>
|
|
</div>
|
|
{(header.selectionLabel || header.floatingActions) ? (
|
|
<div className="panel-floating-region" aria-live="polite" aria-atomic="true">
|
|
<div className="panel-floating">
|
|
{header.selectionLabel ? (
|
|
<span className="panel-floating__label">{header.selectionLabel}</span>
|
|
) : null}
|
|
{header.floatingActions || null}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</>
|
|
) : null}
|
|
<div className={bodyClass}>{surface.content}</div>
|
|
{surfaceDetail}
|
|
</div>
|
|
</DocumentsLayout>
|
|
);
|
|
};
|
|
|
|
const DocumentsRoute: React.FC = () => (
|
|
<SidebarProvider>
|
|
<PanelManagerProvider>
|
|
<DocumentsRouteContent />
|
|
</PanelManagerProvider>
|
|
</SidebarProvider>
|
|
);
|
|
|
|
export default DocumentsRoute;
|