This commit is contained in:
2025-11-19 00:44:47 +01:00
parent 3e21e72953
commit 1764f24243
6 changed files with 173 additions and 309 deletions
+66 -38
View File
@@ -1,8 +1,8 @@
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { SidebarExpandIcon } from '../ui/icons'; import { SidebarExpandIcon } from '../ui/icons';
import { createDocumentsSurface } from '../documents/DocumentsPanel'; import DocumentsPanel from '../documents/panel/DocumentsPanel';
import { createDocumentViewerSurface } from '../preview/DocumentViewerPanel'; import DocumentViewerPanel from '../preview/DocumentViewerPanel';
import { usePanelManager } from './PanelManagerContext'; import { usePanelManager } from './PanelManagerContext';
type Identifier = string | number; type Identifier = string | number;
@@ -13,9 +13,7 @@ type GetDocumentAsset = (document: unknown, assetType: string) => unknown;
type ResolveApiPath = (path: string) => string; type ResolveApiPath = (path: string) => string;
type NotifyApiError = (error: unknown, fallbackMessage?: string) => void; type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
type DocumentsSurface = ReturnType<typeof createDocumentsSurface> | null; type WorkspaceSurface = { content: ReactNode; detail?: ReactNode | null } | null;
type PreviewSurface = ReturnType<typeof createDocumentViewerSurface> | null;
type WorkspaceSurface = DocumentsSurface | PreviewSurface | null;
interface UseWorkspaceSurfaceArgs { interface UseWorkspaceSurfaceArgs {
sidebarHidden?: boolean; sidebarHidden?: boolean;
@@ -84,29 +82,52 @@ export const useWorkspaceSurface = ({
); );
}, [sidebarHidden, onExpandSidebar]); }, [sidebarHidden, onExpandSidebar]);
const documentsSurface = useMemo<DocumentsSurface>(() => { const documentsSurface = useMemo<WorkspaceSurface>(() => {
if (!documentsTableProps) { if (!documentsTableProps) {
return null; return null;
} }
return createDocumentsSurface({
tableProps: documentsTableProps, const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
renderSidebarToggle,
detailProps: detailPanelProps, const detail = detailPanelOpen && detailPanelProps
detailOpen: detailPanelOpen, ? (() => {
}); const { onClose, onOpenPreview, tags: tagOptions, ...restDetailProps } = detailPanelProps;
return (
<DocumentViewerPanel
variant="sidebar"
onCollapsePanel={onClose}
onMaximizePanel={onOpenPreview}
tagOptions={tagOptions}
{...restDetailProps}
/>
);
})()
: null;
return {
content: (
<DocumentsPanel
{...documentsTableProps}
headerLeading={sidebarToggle}
/>
),
detail,
};
}, [ }, [
documentsTableProps, documentsTableProps,
renderSidebarToggle, renderSidebarToggle,
detailPanelProps,
detailPanelOpen, detailPanelOpen,
detailPanelProps,
]); ]);
const showPreviewWorkspace = Boolean(previewDocumentId); const showPreviewWorkspace = Boolean(previewDocumentId);
const previewSurface = useMemo<PreviewSurface>(() => { const previewSurface = useMemo<WorkspaceSurface>(() => {
if (!showPreviewWorkspace) { if (!showPreviewWorkspace || !previewWorkspaceDocument) {
return null; return null;
} }
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
const detailExtras = detailPanelProps || {}; const detailExtras = detailPanelProps || {};
const { const {
tagLookupById, tagLookupById,
@@ -120,38 +141,45 @@ export const useWorkspaceSurface = ({
onUpdateIssued, onUpdateIssued,
resolveFolderPath, resolveFolderPath,
} = detailExtras; } = detailExtras;
return createDocumentViewerSurface({
document: previewWorkspaceDocument, return {
documentLink, content: (
ensureAssetUrl, <DocumentViewerPanel
ensurePreviewData, document={previewWorkspaceDocument}
getDocumentAsset, documentLink={documentLink}
resolveApiPath, hydrateDocument={ensurePreviewData}
notifyApiError, tagLookupById={tagLookupById}
onClose: closeDocumentPreview, tagOptions={tagOptions}
renderSidebarToggle, onTagAdd={onTagAdd}
tagLookupById, onTagRemove={onTagRemove}
tagOptions, correspondents={correspondents}
onTagAdd, onCorrespondentAdd={onCorrespondentAdd}
onTagRemove, onCorrespondentRemove={onCorrespondentRemove}
correspondents, onUpdateTitle={onUpdateTitle}
onCorrespondentAdd, onUpdateIssued={onUpdateIssued}
onCorrespondentRemove, ensureAssetUrl={ensureAssetUrl}
onUpdateTitle, getDocumentAsset={getDocumentAsset}
onUpdateIssued, ensurePreviewData={ensurePreviewData}
resolveFolderPath, resolveApiPath={resolveApiPath}
}); notifyApiError={notifyApiError}
sidebarToggle={sidebarToggle}
onClosePanel={closeDocumentPreview}
resolveFolderPath={resolveFolderPath}
/>
),
detail: null,
};
}, [ }, [
showPreviewWorkspace, showPreviewWorkspace,
previewWorkspaceDocument, previewWorkspaceDocument,
documentLink, documentLink,
ensureAssetUrl,
ensurePreviewData, ensurePreviewData,
ensureAssetUrl,
getDocumentAsset, getDocumentAsset,
resolveApiPath, resolveApiPath,
notifyApiError, notifyApiError,
closeDocumentPreview,
renderSidebarToggle, renderSidebarToggle,
closeDocumentPreview,
detailPanelProps, detailPanelProps,
]); ]);
-1
View File
@@ -1,3 +1,2 @@
export { default } from './panel/DocumentsPanel'; export { default } from './panel/DocumentsPanel';
export { default as createDocumentsSurface } from './panel/createDocumentsSurface';
export { createDocumentsTableHeaderActions } from './panel/DocumentsToolbar'; export { createDocumentsTableHeaderActions } from './panel/DocumentsToolbar';
+103 -6
View File
@@ -11,6 +11,8 @@ import DocumentsPanelHeader, {
DocumentsPanelHeaderConfig, DocumentsPanelHeaderConfig,
DocumentsHeaderBreadcrumb, DocumentsHeaderBreadcrumb,
} from './DocumentsPanelHeader'; } from './DocumentsPanelHeader';
import { SelectionFloatingPanel } from '../SelectionFloatingActions';
import { createDocumentsTableHeaderActions } from './DocumentsToolbar';
const DEFAULT_GRID_ICON_SIZE = 144; const DEFAULT_GRID_ICON_SIZE = 144;
@@ -20,7 +22,7 @@ const EntryType = {
}; };
interface DocumentsPanelProps { interface DocumentsPanelProps {
headerConfig?: DocumentsPanelHeaderConfig; headerLeading?: ReactNode;
onBreadcrumbNavigate?: (crumb: DocumentsHeaderBreadcrumb) => void; onBreadcrumbNavigate?: (crumb: DocumentsHeaderBreadcrumb) => void;
[key: string]: any; [key: string]: any;
} }
@@ -30,9 +32,9 @@ const defaultGetDocumentAsset = (_doc?: unknown, _type?: string) => null;
export type DocumentLinkLike = { url?: string | null; contentType?: string | null }; export type DocumentLinkLike = { url?: string | null; contentType?: string | null };
const DocumentsPanel: React.FC<DocumentsPanelProps> = ({ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
headerConfig, headerLeading = null,
onBreadcrumbNavigate, onBreadcrumbNavigate,
currentFolderName: _currentFolderName, currentFolderName,
breadcrumbs, breadcrumbs,
subfolders, subfolders,
documents, documents,
@@ -61,10 +63,28 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
isSearchLoading = false, isSearchLoading = false,
onDocumentTagDrop, onDocumentTagDrop,
viewMode = 'list', viewMode = 'list',
onViewModeChange: _onViewModeChange, onViewModeChange,
documentLinks, documentLinks,
ensureDownloadUrl, ensureDownloadUrl,
deskWorkspaceProps = null, deskWorkspaceProps = null,
onRefresh = () => {},
sortField,
sortDirection,
onSortFieldChange,
onSortDirectionToggle,
searchIncludeDescendants,
onToggleSearchIncludeDescendants,
onDeleteSelection,
documentLookup,
tags,
correspondents,
onBulkTagAdd,
onBulkTagRemove,
onBulkCorrespondentAdd,
onBulkCorrespondentRemove,
onBulkReanalyze,
folderOptions,
onMoveDocumentsToFolder,
}): ReactNode => { }): ReactNode => {
const { const {
selectedEntries, selectedEntries,
@@ -76,6 +96,85 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
const showingSearchResults = searchResults !== null; const showingSearchResults = searchResults !== null;
const rows = showingSearchResults ? searchResults : documents; const rows = showingSearchResults ? searchResults : documents;
const documentLinkMap = documentLinks instanceof Map ? documentLinks : null; const documentLinkMap = documentLinks instanceof Map ? documentLinks : null;
const searchResultCount = Array.isArray(searchResults) ? searchResults.length : 0;
const headerTitle = showingSearchResults
? 'Search results'
: currentFolderName || 'Documents';
const headerSubtitle = showingSearchResults
? `${searchResultCount} matching document${searchResultCount === 1 ? '' : 's'}`
: null;
const headerActions = useMemo(
() => createDocumentsTableHeaderActions({
viewMode,
onViewModeChange,
onRefresh,
sortField,
onSortFieldChange,
sortDirection,
onSortDirectionToggle,
isFilterActive,
includeDescendants: searchIncludeDescendants,
onToggleIncludeDescendants: onToggleSearchIncludeDescendants,
}),
[
viewMode,
onViewModeChange,
onRefresh,
sortField,
onSortFieldChange,
sortDirection,
onSortDirectionToggle,
isFilterActive,
searchIncludeDescendants,
onToggleSearchIncludeDescendants,
],
);
const floatingActions = useMemo(() => (
<SelectionFloatingPanel
documentLookup={documentLookup}
tags={tags}
tagLookupById={tagLookupById}
correspondents={correspondents}
onBulkTagAdd={onBulkTagAdd}
onBulkTagRemove={onBulkTagRemove}
onBulkCorrespondentAdd={onBulkCorrespondentAdd}
onBulkCorrespondentRemove={onBulkCorrespondentRemove}
onBulkReanalyze={onBulkReanalyze}
onDeleteSelection={onDeleteSelection}
folderOptions={folderOptions}
onMoveDocumentsToFolder={onMoveDocumentsToFolder}
onClearSelection={clearSelection}
/>
), [
documentLookup,
tags,
tagLookupById,
correspondents,
onBulkTagAdd,
onBulkTagRemove,
onBulkCorrespondentAdd,
onBulkCorrespondentRemove,
onBulkReanalyze,
onDeleteSelection,
folderOptions,
onMoveDocumentsToFolder,
clearSelection,
]);
const headerConfig: DocumentsPanelHeaderConfig = useMemo(() => ({
title: headerTitle,
subtitle: headerSubtitle,
leading: headerLeading,
actions: headerActions,
breadcrumbs,
floatingActions,
}), [
headerTitle,
headerSubtitle,
headerLeading,
headerActions,
breadcrumbs,
floatingActions,
]);
const currentFolderId = useMemo(() => { const currentFolderId = useMemo(() => {
if (showingSearchResults) { if (showingSearchResults) {
@@ -702,12 +801,10 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
return ( return (
<> <>
{headerConfig ? (
<DocumentsPanelHeader <DocumentsPanelHeader
header={headerConfig} header={headerConfig}
onBreadcrumbClick={onBreadcrumbNavigate} onBreadcrumbClick={onBreadcrumbNavigate}
/> />
) : null}
<section <section
ref={scrollRef} ref={scrollRef}
className={`documents-panel documents-panel--view-${panelVariant}`} className={`documents-panel documents-panel--view-${panelVariant}`}
@@ -1,134 +0,0 @@
import React, { ReactNode } from 'react';
import DocumentViewerPanel from '../../preview/DocumentViewerPanel';
import createWorkspaceSurfaceConfig from '../workspaceHeader';
import DocumentsPanel from './DocumentsPanel';
import type { DocumentsPanelHeaderConfig } from './DocumentsPanelHeader';
import { SelectionFloatingPanel } from '../SelectionFloatingActions';
import { createDocumentsTableHeaderActions } from './DocumentsToolbar';
interface CreateDocumentsSurfaceArgs {
tableProps: Record<string, any>;
renderSidebarToggle?: () => ReactNode;
detailProps?: Record<string, any> | null;
detailOpen?: boolean;
}
const createDocumentsSurface = ({
tableProps,
renderSidebarToggle,
detailProps,
detailOpen = false,
}: CreateDocumentsSurfaceArgs) => {
const {
currentFolderName,
breadcrumbs,
searchResults,
isFilterActive,
viewMode,
onViewModeChange,
onRefresh,
sortField,
sortDirection,
onSortFieldChange,
onSortDirectionToggle,
onDeleteSelection,
tags,
correspondents,
documentLookup,
tagLookupById,
onBulkTagAdd,
onBulkTagRemove,
onBulkCorrespondentAdd,
onBulkCorrespondentRemove,
onBulkReanalyze,
folderOptions,
onMoveDocumentsToFolder,
searchIncludeDescendants,
onToggleSearchIncludeDescendants,
onInspectDocument,
} = tableProps;
const isDeskView = viewMode === 'desk';
const title = Array.isArray(searchResults) ? 'Search results' : currentFolderName;
const subtitle = Array.isArray(searchResults)
? `${searchResults.length} matching document${searchResults.length === 1 ? '' : 's'}`
: null;
const actions = createDocumentsTableHeaderActions({
viewMode,
onViewModeChange,
onRefresh,
sortField,
onSortFieldChange,
sortDirection,
onSortDirectionToggle,
isFilterActive,
includeDescendants: searchIncludeDescendants,
onToggleIncludeDescendants: onToggleSearchIncludeDescendants,
});
const floatingActions = (
<SelectionFloatingPanel
documentLookup={documentLookup}
tags={tags}
tagLookupById={tagLookupById}
correspondents={correspondents}
onBulkTagAdd={onBulkTagAdd}
onBulkTagRemove={onBulkTagRemove}
onBulkCorrespondentAdd={onBulkCorrespondentAdd}
onBulkCorrespondentRemove={onBulkCorrespondentRemove}
onBulkReanalyze={onBulkReanalyze}
onDeleteSelection={onDeleteSelection}
folderOptions={folderOptions}
onMoveDocumentsToFolder={onMoveDocumentsToFolder}
/>
);
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
const detail = detailOpen && detailProps
? (() => {
const { onClose, onOpenPreview, tags: tagOptions, ...restDetailProps } = detailProps;
return (
<DocumentViewerPanel
variant="sidebar"
onCollapsePanel={onClose}
onMaximizePanel={onOpenPreview}
tagOptions={tagOptions}
{...restDetailProps}
/>
);
})()
: null;
const headerConfig: DocumentsPanelHeaderConfig = {
title,
subtitle,
leading: sidebarToggle,
actions,
breadcrumbs,
floatingActions,
};
return createWorkspaceSurfaceConfig({
key: isDeskView ? 'workspace' : 'documents',
variant: isDeskView ? 'workspace' : 'documents',
title,
subtitle,
sidebarToggle,
actions,
breadcrumbs,
selectionLabel: null,
floatingActions,
content: (
<DocumentsPanel
{...tableProps}
headerConfig={headerConfig}
onInspectDocument={onInspectDocument}
/>
),
detail,
});
};
export default createDocumentsSurface;
@@ -1,67 +0,0 @@
import { ReactNode } from 'react';
interface WorkspaceSurfaceHeaderConfig {
title: ReactNode;
subtitle?: ReactNode;
leading?: ReactNode;
actions?: ReactNode;
breadcrumbs?: ReactNode;
selectionLabel?: ReactNode;
floatingActions?: ReactNode;
}
interface WorkspaceSurfaceConfig {
key: string;
variant: string;
header: WorkspaceSurfaceHeaderConfig;
content?: ReactNode;
detail?: ReactNode;
}
interface CreateWorkspaceSurfaceConfigArgs {
title: ReactNode;
subtitle?: ReactNode;
sidebarToggle?: ReactNode;
actions?: ReactNode;
breadcrumbs?: ReactNode;
selectionLabel?: ReactNode;
floatingActions?: ReactNode;
content?: ReactNode;
detail?: ReactNode;
variant?: string;
key?: string;
}
export const createWorkspaceSurfaceConfig = ({
title,
subtitle = null,
sidebarToggle = null,
actions = null,
breadcrumbs = null,
selectionLabel = null,
floatingActions = null,
content = null,
detail = null,
variant = 'documents',
key = 'documents',
}: CreateWorkspaceSurfaceConfigArgs): WorkspaceSurfaceConfig => {
const leading = sidebarToggle ? <>{sidebarToggle}</> : null;
return {
key,
variant,
header: {
title,
subtitle,
leading,
actions,
breadcrumbs,
selectionLabel,
floatingActions,
},
content,
detail,
};
};
export default createWorkspaceSurfaceConfig;
@@ -546,62 +546,3 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
}; };
export default DocumentViewerPanel; export default DocumentViewerPanel;
export const createDocumentViewerSurface = ({
document,
documentLink,
ensureAssetUrl,
ensurePreviewData,
getDocumentAsset,
resolveApiPath,
notifyApiError,
onClose,
renderSidebarToggle,
tagLookupById,
tagOptions,
onTagAdd,
onTagRemove,
correspondents,
onCorrespondentAdd,
onCorrespondentRemove,
onUpdateTitle,
onUpdateIssued,
resolveFolderPath,
}) => {
if (!document) {
return null;
}
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
return {
key: 'preview',
variant: 'preview',
header: null,
content: (
<DocumentViewerPanel
document={document}
documentLink={documentLink}
hydrateDocument={ensurePreviewData}
tagLookupById={tagLookupById}
tagOptions={tagOptions}
onTagAdd={onTagAdd}
onTagRemove={onTagRemove}
correspondents={correspondents}
onCorrespondentAdd={onCorrespondentAdd}
onCorrespondentRemove={onCorrespondentRemove}
onUpdateTitle={onUpdateTitle}
onUpdateIssued={onUpdateIssued}
ensureAssetUrl={ensureAssetUrl}
getDocumentAsset={getDocumentAsset}
ensurePreviewData={ensurePreviewData}
resolveApiPath={resolveApiPath}
notifyApiError={notifyApiError}
sidebarToggle={sidebarToggle}
onClosePanel={onClose}
resolveFolderPath={resolveFolderPath}
/>
),
supportsDetail: false,
};
};