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 type { ReactNode } from 'react';
import { SidebarExpandIcon } from '../ui/icons';
import { createDocumentsSurface } from '../documents/DocumentsPanel';
import { createDocumentViewerSurface } from '../preview/DocumentViewerPanel';
import DocumentsPanel from '../documents/panel/DocumentsPanel';
import DocumentViewerPanel from '../preview/DocumentViewerPanel';
import { usePanelManager } from './PanelManagerContext';
type Identifier = string | number;
@@ -13,9 +13,7 @@ type GetDocumentAsset = (document: unknown, assetType: string) => unknown;
type ResolveApiPath = (path: string) => string;
type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
type DocumentsSurface = ReturnType<typeof createDocumentsSurface> | null;
type PreviewSurface = ReturnType<typeof createDocumentViewerSurface> | null;
type WorkspaceSurface = DocumentsSurface | PreviewSurface | null;
type WorkspaceSurface = { content: ReactNode; detail?: ReactNode | null } | null;
interface UseWorkspaceSurfaceArgs {
sidebarHidden?: boolean;
@@ -84,29 +82,52 @@ export const useWorkspaceSurface = ({
);
}, [sidebarHidden, onExpandSidebar]);
const documentsSurface = useMemo<DocumentsSurface>(() => {
const documentsSurface = useMemo<WorkspaceSurface>(() => {
if (!documentsTableProps) {
return null;
}
return createDocumentsSurface({
tableProps: documentsTableProps,
renderSidebarToggle,
detailProps: detailPanelProps,
detailOpen: detailPanelOpen,
});
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
const detail = detailPanelOpen && detailPanelProps
? (() => {
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,
renderSidebarToggle,
detailPanelProps,
detailPanelOpen,
detailPanelProps,
]);
const showPreviewWorkspace = Boolean(previewDocumentId);
const previewSurface = useMemo<PreviewSurface>(() => {
if (!showPreviewWorkspace) {
const previewSurface = useMemo<WorkspaceSurface>(() => {
if (!showPreviewWorkspace || !previewWorkspaceDocument) {
return null;
}
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
const detailExtras = detailPanelProps || {};
const {
tagLookupById,
@@ -120,38 +141,45 @@ export const useWorkspaceSurface = ({
onUpdateIssued,
resolveFolderPath,
} = detailExtras;
return createDocumentViewerSurface({
document: previewWorkspaceDocument,
documentLink,
ensureAssetUrl,
ensurePreviewData,
getDocumentAsset,
resolveApiPath,
notifyApiError,
onClose: closeDocumentPreview,
renderSidebarToggle,
tagLookupById,
tagOptions,
onTagAdd,
onTagRemove,
correspondents,
onCorrespondentAdd,
onCorrespondentRemove,
onUpdateTitle,
onUpdateIssued,
resolveFolderPath,
});
return {
content: (
<DocumentViewerPanel
document={previewWorkspaceDocument}
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={closeDocumentPreview}
resolveFolderPath={resolveFolderPath}
/>
),
detail: null,
};
}, [
showPreviewWorkspace,
previewWorkspaceDocument,
documentLink,
ensureAssetUrl,
ensurePreviewData,
ensureAssetUrl,
getDocumentAsset,
resolveApiPath,
notifyApiError,
closeDocumentPreview,
renderSidebarToggle,
closeDocumentPreview,
detailPanelProps,
]);
-1
View File
@@ -1,3 +1,2 @@
export { default } from './panel/DocumentsPanel';
export { default as createDocumentsSurface } from './panel/createDocumentsSurface';
export { createDocumentsTableHeaderActions } from './panel/DocumentsToolbar';
+107 -10
View File
@@ -11,6 +11,8 @@ import DocumentsPanelHeader, {
DocumentsPanelHeaderConfig,
DocumentsHeaderBreadcrumb,
} from './DocumentsPanelHeader';
import { SelectionFloatingPanel } from '../SelectionFloatingActions';
import { createDocumentsTableHeaderActions } from './DocumentsToolbar';
const DEFAULT_GRID_ICON_SIZE = 144;
@@ -20,7 +22,7 @@ const EntryType = {
};
interface DocumentsPanelProps {
headerConfig?: DocumentsPanelHeaderConfig;
headerLeading?: ReactNode;
onBreadcrumbNavigate?: (crumb: DocumentsHeaderBreadcrumb) => void;
[key: string]: any;
}
@@ -30,9 +32,9 @@ const defaultGetDocumentAsset = (_doc?: unknown, _type?: string) => null;
export type DocumentLinkLike = { url?: string | null; contentType?: string | null };
const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
headerConfig,
headerLeading = null,
onBreadcrumbNavigate,
currentFolderName: _currentFolderName,
currentFolderName,
breadcrumbs,
subfolders,
documents,
@@ -61,10 +63,28 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
isSearchLoading = false,
onDocumentTagDrop,
viewMode = 'list',
onViewModeChange: _onViewModeChange,
onViewModeChange,
documentLinks,
ensureDownloadUrl,
deskWorkspaceProps = null,
onRefresh = () => {},
sortField,
sortDirection,
onSortFieldChange,
onSortDirectionToggle,
searchIncludeDescendants,
onToggleSearchIncludeDescendants,
onDeleteSelection,
documentLookup,
tags,
correspondents,
onBulkTagAdd,
onBulkTagRemove,
onBulkCorrespondentAdd,
onBulkCorrespondentRemove,
onBulkReanalyze,
folderOptions,
onMoveDocumentsToFolder,
}): ReactNode => {
const {
selectedEntries,
@@ -76,6 +96,85 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
const showingSearchResults = searchResults !== null;
const rows = showingSearchResults ? searchResults : documents;
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(() => {
if (showingSearchResults) {
@@ -702,12 +801,10 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
return (
<>
{headerConfig ? (
<DocumentsPanelHeader
header={headerConfig}
onBreadcrumbClick={onBreadcrumbNavigate}
/>
) : null}
<DocumentsPanelHeader
header={headerConfig}
onBreadcrumbClick={onBreadcrumbNavigate}
/>
<section
ref={scrollRef}
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 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,
};
};