This commit is contained in:
2025-11-18 13:16:35 +01:00
parent 0b0a1a88fd
commit 0c37ef5578
20 changed files with 434 additions and 417 deletions
+118 -120
View File
@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { ReactNode } from 'react';
import DocumentsGrid from '../DocumentsGrid';
import DocumentsList from '../DocumentsList';
import DesktopWorkspace from '../../desktop/DesktopWorkspace';
@@ -6,6 +7,10 @@ import { isTagTransferEvent } from '../tagTransfer';
import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay';
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../useEntryPointer';
import { useWorkspaceSelectionContext } from '../../app/WorkspaceSelectionContext';
import DocumentsPanelHeader, {
DocumentsPanelHeaderConfig,
DocumentsHeaderBreadcrumb,
} from './DocumentsPanelHeader';
const DEFAULT_GRID_ICON_SIZE = 144;
@@ -15,6 +20,8 @@ const EntryType = {
};
interface DocumentsPanelProps {
headerConfig?: DocumentsPanelHeaderConfig;
onBreadcrumbNavigate?: (crumb: DocumentsHeaderBreadcrumb) => void;
[key: string]: any;
}
@@ -23,6 +30,8 @@ const defaultGetDocumentAsset = (_doc?: unknown, _type?: string) => null;
export type PreviewEntryLike = { url?: string | null; contentType?: string | null };
const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
headerConfig,
onBreadcrumbNavigate,
currentFolderName: _currentFolderName,
breadcrumbs,
subfolders,
@@ -56,7 +65,7 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
previewEntries,
ensureDownloadUrl,
deskWorkspaceProps = null,
}) => {
}): ReactNode => {
const {
selectedEntries,
focusedRowKey,
@@ -123,20 +132,8 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
() => new Set(activeCorrespondentIds || []),
[activeCorrespondentIds],
);
const scrollRef = useRef(null);
const scrollRef = useRef<HTMLElement | null>(null);
const suppressDocumentClickRef = useRef(false);
const [, forceVisibilityTick] = useState(0);
const lastScrollNodeRef = useRef(null);
const assignScrollRef = useCallback((node) => {
if (lastScrollNodeRef.current === node) {
return;
}
lastScrollNodeRef.current = node;
scrollRef.current = node;
if (node) {
forceVisibilityTick((value) => value + 1);
}
}, []);
const isGridView = viewMode === 'grid';
const isDeskView = viewMode === 'desk';
@@ -577,44 +574,34 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
const showGridSearchEmptyState = isGridView && showingSearchResults && !hasDocumentEntries && !isSearchLoading;
const renderBody = () => {
if (isDeskView) {
return (
<div className="panel-section__body panel-section__body--desk">
{deskWorkspaceProps ? (
<DesktopWorkspace {...deskWorkspaceProps} />
) : (
<div className="empty-state empty-state--global">
Desk view is unavailable.
</div>
)}
return deskWorkspaceProps ? (
<DesktopWorkspace {...deskWorkspaceProps} />
) : (
<div className="empty-state empty-state--global">
Desk view is unavailable.
</div>
);
}
if (showDefaultEmptyState) {
return (
<div className="panel-section__body">
<div className="empty-state">
Drop files anywhere or onto a folder to upload documents.
</div>
<div className="empty-state">
Drop files anywhere or onto a folder to upload documents.
</div>
);
}
if (showGridSearchEmptyState) {
return (
<div className="panel-section__body">
<div className="empty-state empty-state--global">
No documents match the current filters.
</div>
<div className="empty-state empty-state--global">
No documents match the current filters.
</div>
);
}
if (showListSearchEmptyState) {
return (
<div className="panel-section__body">
<div className="empty-state">No documents match the current filters.</div>
</div>
<div className="empty-state">No documents match the current filters.</div>
);
}
@@ -622,102 +609,113 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
return null;
}
if (isGridView) {
return (
<DocumentsGrid
entries={entries}
draggingDocumentIdsSet={draggingSet}
draggedFolderId={draggedFolderId}
onFolderClick={handleFolderClick}
onFolderSelect={onFolderSelect}
onFolderDragOver={onFolderDragOver}
onFolderDragLeave={onFolderDragLeave}
onFolderDrop={onFolderDrop}
onFolderDragStart={onFolderDragStart}
onFolderDragEnd={onFolderDragEnd}
onDocumentClick={handleDocumentClick}
onDocumentActivate={handleDocumentActivate}
onDocumentDragStart={handleDocumentDragStartLocal}
onDocumentDragEnd={handleDocumentDragEndLocal}
onDocumentTagDragOver={handleDocumentTagDragOver}
onDocumentTagDragLeave={handleDocumentTagDragLeave}
onDocumentTagDrop={onDocumentTagDrop}
ensureAssetUrl={ensureAssetUrl}
getDocumentAsset={getDocumentAsset}
gridIconSize={gridIconSize}
tagLookupById={tagLookupById}
onTagClick={onTagClick}
scrollRef={scrollRef}
onCorrespondentClick={onCorrespondentClick}
activeCorrespondentIdSet={activeCorrespondentIdSet}
onDocumentRename={onDocumentRename}
onFolderRename={onFolderRename}
/>
);
}
return (
<div className="panel-section__body">
<div
ref={assignScrollRef}
className="documents-scroll"
tabIndex={0}
onFocus={(event) => {
if (event.target === scrollRef.current) {
handlePanelFocus();
}
}}
onKeyDown={(event) => {
if (event.target !== scrollRef.current) {
return;
}
handlePanelKeyDown(event);
}}
onClick={(event) => {
if (event.target === event.currentTarget) {
clearSelection();
}
}}
aria-activedescendant={isGridView ? undefined : activeDescendantId}
>
{isGridView ? (
<DocumentsGrid
entries={entries}
draggingDocumentIdsSet={draggingSet}
draggedFolderId={draggedFolderId}
onFolderClick={handleFolderClick}
onFolderSelect={onFolderSelect}
onFolderDragOver={onFolderDragOver}
onFolderDragLeave={onFolderDragLeave}
onFolderDrop={onFolderDrop}
onFolderDragStart={onFolderDragStart}
onFolderDragEnd={onFolderDragEnd}
onDocumentClick={handleDocumentClick}
onDocumentActivate={handleDocumentActivate}
onDocumentDragStart={handleDocumentDragStartLocal}
onDocumentDragEnd={handleDocumentDragEndLocal}
onDocumentTagDragOver={handleDocumentTagDragOver}
onDocumentTagDragLeave={handleDocumentTagDragLeave}
onDocumentTagDrop={handleDocumentTagDrop}
ensureAssetUrl={ensureAssetUrl}
getDocumentAsset={getDocumentAsset}
gridIconSize={gridIconSize}
tagLookupById={tagLookupById}
onTagClick={onTagClick}
scrollRef={scrollRef}
onCorrespondentClick={onCorrespondentClick}
activeCorrespondentIdSet={activeCorrespondentIdSet}
onDocumentRename={onDocumentRename}
onFolderRename={onFolderRename}
/>
) : (
<DocumentsList
entries={entries}
focusedRowKey={focusedRowKey}
draggingDocumentIdsSet={draggingSet}
draggedFolderId={draggedFolderId}
ensureAssetUrl={ensureAssetUrl}
getDocumentAsset={getDocumentAsset}
onFolderClick={handleFolderClick}
onFolderSelect={onFolderSelect}
onFolderDragOver={onFolderDragOver}
onFolderDragLeave={onFolderDragLeave}
onFolderDrop={onFolderDrop}
onFolderDragStart={onFolderDragStart}
onFolderDragEnd={onFolderDragEnd}
onFolderRename={onFolderRename}
onDocumentClick={handleDocumentClick}
onDocumentActivate={handleDocumentActivate}
onDocumentDragStart={handleDocumentDragStartLocal}
onDocumentDragEnd={handleDocumentDragEndLocal}
onDocumentTagDragOver={handleDocumentTagDragOver}
onDocumentTagDragLeave={handleDocumentTagDragLeave}
onDocumentTagDrop={handleDocumentTagDrop}
onDocumentRename={onDocumentRename}
tagLookupById={tagLookupById}
onTagClick={onTagClick}
onCorrespondentClick={onCorrespondentClick}
activeCorrespondentIdSet={activeCorrespondentIdSet}
scrollRef={scrollRef}
/>
)}
</div>
</div>
<DocumentsList
entries={entries}
focusedRowKey={focusedRowKey}
draggingDocumentIdsSet={draggingSet}
draggedFolderId={draggedFolderId}
ensureAssetUrl={ensureAssetUrl}
getDocumentAsset={getDocumentAsset}
onFolderClick={handleFolderClick}
onFolderSelect={onFolderSelect}
onFolderDragOver={onFolderDragOver}
onFolderDragLeave={onFolderDragLeave}
onFolderDrop={onFolderDrop}
onFolderDragStart={onFolderDragStart}
onFolderDragEnd={onFolderDragEnd}
onFolderRename={onFolderRename}
onDocumentClick={handleDocumentClick}
onDocumentActivate={handleDocumentActivate}
onDocumentDragStart={handleDocumentDragStartLocal}
onDocumentDragEnd={handleDocumentDragEndLocal}
onDocumentTagDragOver={handleDocumentTagDragOver}
onDocumentTagDragLeave={handleDocumentTagDragLeave}
onDocumentTagDrop={handleDocumentTagDrop}
onDocumentRename={onDocumentRename}
tagLookupById={tagLookupById}
onTagClick={onTagClick}
onCorrespondentClick={onCorrespondentClick}
activeCorrespondentIdSet={activeCorrespondentIdSet}
scrollRef={scrollRef}
/>
);
};
const panelVariant = isDeskView ? 'desk' : isGridView ? 'grid' : 'list';
const shouldHandlePanelInteractions = !isDeskView && showTableRows;
const handleSectionFocus = useCallback((event: React.FocusEvent<HTMLElement>) => {
if (!shouldHandlePanelInteractions || event.target !== event.currentTarget) {
return;
}
handlePanelFocus();
}, [shouldHandlePanelInteractions, handlePanelFocus]);
const handleSectionKeyDown = useCallback((event: React.KeyboardEvent<HTMLElement>) => {
if (!shouldHandlePanelInteractions || event.target !== event.currentTarget) {
return;
}
handlePanelKeyDown(event);
}, [shouldHandlePanelInteractions, handlePanelKeyDown]);
const handleSectionClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
if (!shouldHandlePanelInteractions || event.target !== event.currentTarget) {
return;
}
clearSelection();
}, [shouldHandlePanelInteractions, clearSelection]);
return (
<>
{headerConfig ? (
<DocumentsPanelHeader
header={headerConfig}
onBreadcrumbClick={onBreadcrumbNavigate}
/>
) : null}
<section
ref={scrollRef}
className={`documents-panel documents-panel--view-${panelVariant}`}
tabIndex={shouldHandlePanelInteractions ? 0 : undefined}
onFocus={handleSectionFocus}
onKeyDown={handleSectionKeyDown}
onClick={handleSectionClick}
aria-activedescendant={shouldHandlePanelInteractions && !isGridView ? activeDescendantId : undefined}
>
{renderBody()}
</section>
@@ -0,0 +1,73 @@
import React from 'react';
import type { ReactNode } from 'react';
import PanelHeader from '../../ui/PanelHeader';
import BreadcrumbTrail from '../../ui/BreadcrumbTrail';
type Identifier = string | number;
export interface DocumentsHeaderBreadcrumb {
id?: Identifier;
name?: string;
label?: string;
title?: string;
}
export interface DocumentsPanelHeaderConfig {
title?: ReactNode;
subtitle?: ReactNode;
leading?: ReactNode;
actions?: ReactNode;
breadcrumbs?: DocumentsHeaderBreadcrumb[] | null;
floatingActions?: ReactNode;
}
interface DocumentsPanelHeaderProps {
header?: DocumentsPanelHeaderConfig | null;
onBreadcrumbClick?: (crumb: DocumentsHeaderBreadcrumb) => void;
}
const DocumentsPanelHeader: React.FC<DocumentsPanelHeaderProps> = ({
header,
onBreadcrumbClick,
}) => {
if (!header) {
return null;
}
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 && onBreadcrumbClick
? () => onBreadcrumbClick(crumb)
: null,
}))
: [{ id: 'current-location', label: header.title }];
const headerTitle = (
<h2>
<BreadcrumbTrail entries={trailEntries} separator="/" />
{header.subtitle ? (
<span className="panel-header__subtitle">{header.subtitle}</span>
) : null}
</h2>
);
return (
<>
<PanelHeader
leading={header.leading}
title={headerTitle}
titleTag="h2"
actions={header.actions}
/>
{header.floatingActions}
</>
);
};
export default DocumentsPanelHeader;
@@ -111,7 +111,7 @@ export const createDocumentsTableHeaderActions = ({
<div className="view-toggle" role="group" aria-label="Change view">
<button
type="button"
className={`view-toggle__button${isListView ? ' active' : ''}`}
className={`toggle-button${isListView ? ' active' : ''}`}
onClick={() => onViewModeChange?.('list')}
aria-pressed={isListView}
title="List view"
@@ -120,7 +120,7 @@ export const createDocumentsTableHeaderActions = ({
</button>
<button
type="button"
className={`view-toggle__button${isGridView ? ' active' : ''}`}
className={`toggle-button${isGridView ? ' active' : ''}`}
onClick={() => onViewModeChange?.('grid')}
aria-pressed={isGridView}
title="Icons view"
@@ -129,7 +129,7 @@ export const createDocumentsTableHeaderActions = ({
</button>
<button
type="button"
className={`view-toggle__button${isDeskView ? ' active' : ''}`}
className={`toggle-button${isDeskView ? ' active' : ''}`}
onClick={() => onViewModeChange?.('desk')}
aria-pressed={isDeskView}
title="Desk view"
@@ -50,7 +50,7 @@ const SortFieldQuickMenu: React.FC<SortFieldQuickMenuProps> = ({ sortField, onCh
className="documents-sort__quickmenu"
options={options}
onSelectOption={handleSelect}
triggerClassName="view-toggle__button documents-sort__trigger quick-add__trigger"
triggerClassName="toggle-button documents-sort__trigger quick-add__trigger"
triggerContent={(
<span className="documents-sort__trigger-content">
<span className="documents-sort__label">{label}</span>
@@ -2,6 +2,7 @@ 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';
@@ -100,6 +101,15 @@ const createDocumentsSurface = ({
})()
: null;
const headerConfig: DocumentsPanelHeaderConfig = {
title,
subtitle,
leading: sidebarToggle,
actions,
breadcrumbs,
floatingActions,
};
return createWorkspaceSurfaceConfig({
key: isDeskView ? 'workspace' : 'documents',
variant: isDeskView ? 'workspace' : 'documents',
@@ -113,6 +123,7 @@ const createDocumentsSurface = ({
content: (
<DocumentsPanel
{...tableProps}
headerConfig={headerConfig}
onInspectDocument={onInspectDocument}
/>
),