This commit is contained in:
2025-11-18 00:41:49 +01:00
parent bc9be96123
commit 0b0a1a88fd
11 changed files with 253 additions and 702 deletions
-1
View File
@@ -33,7 +33,6 @@ const AppLayout: React.FC = () => {
onToggleSearchIncludeDescendants: documentsPreferences.toggleSearchIncludeDescendants,
onSetSearchIncludeDescendants: documentsPreferences.setSearchIncludeDescendants,
sortRefreshReadyRef: documentsPreferences.sortRefreshReadyRef,
handleDeskExit: documentsPreferences.handleDeskExit,
});
if (['logged-out', 'authenticating', 'selecting-tenant'].includes(appStatus)) {
-6
View File
@@ -34,8 +34,6 @@ interface DocumentsRouteAppShell {
documentsTableProps?: DocumentsTableProps | null;
detailPanelProps?: Record<string, unknown> | null;
detailPanelOpen?: boolean;
documentsViewMode?: string;
deskWorkspaceProps?: Record<string, unknown> | null;
openTagsModal?: () => void;
openCorrespondentsModal?: () => void;
previewWorkspaceDocument?: unknown;
@@ -55,8 +53,6 @@ const DocumentsRouteContent: React.FC = () => {
documentsTableProps,
detailPanelProps,
detailPanelOpen,
documentsViewMode,
deskWorkspaceProps,
openTagsModal,
openCorrespondentsModal,
previewWorkspaceDocument,
@@ -106,8 +102,6 @@ const DocumentsRouteContent: React.FC = () => {
documentsTableProps,
detailPanelProps,
detailPanelOpen,
viewMode: documentsViewMode,
deskWorkspaceProps,
previewWorkspaceDocument,
previewWorkspaceEntry,
previewDocumentId,
+2 -29
View File
@@ -3,7 +3,6 @@ import type { ReactNode } from 'react';
import { SidebarExpandIcon } from '../ui/icons';
import { createDocumentsSurface } from '../documents/DocumentsPanel';
import { createDocumentViewerSurface } from '../preview/DocumentViewerPanel';
import createDesktopSurface from '../desktop/createDesktopSurface';
import { usePanelManager } from './PanelManagerContext';
type Identifier = string | number;
@@ -16,8 +15,7 @@ type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
type DocumentsSurface = ReturnType<typeof createDocumentsSurface> | null;
type PreviewSurface = ReturnType<typeof createDocumentViewerSurface> | null;
type DesktopSurface = ReturnType<typeof createDesktopSurface> | null;
type WorkspaceSurface = DocumentsSurface | PreviewSurface | DesktopSurface | null;
type WorkspaceSurface = DocumentsSurface | PreviewSurface | null;
interface UseWorkspaceSurfaceArgs {
sidebarHidden?: boolean;
@@ -25,8 +23,6 @@ interface UseWorkspaceSurfaceArgs {
documentsTableProps?: Record<string, any> | null;
detailPanelProps?: (Record<string, any> & { onClose?: () => void }) | null;
detailPanelOpen?: boolean;
viewMode?: string;
deskWorkspaceProps?: Record<string, any> | null;
previewWorkspaceDocument?: unknown;
previewWorkspaceEntry?: unknown;
previewDocumentId?: Identifier | null;
@@ -48,8 +44,6 @@ export const useWorkspaceSurface = ({
documentsTableProps,
detailPanelProps,
detailPanelOpen = false,
viewMode = 'list',
deskWorkspaceProps,
previewWorkspaceDocument,
previewWorkspaceEntry,
previewDocumentId,
@@ -161,33 +155,12 @@ export const useWorkspaceSurface = ({
detailPanelProps,
]);
const workspaceSurface = useMemo<DesktopSurface>(() => {
if (viewMode !== 'desk') {
return null;
}
return createDesktopSurface({
workspaceProps: deskWorkspaceProps,
renderSidebarToggle,
detailProps: detailPanelProps,
detailOpen: detailPanelOpen,
});
}, [
viewMode,
deskWorkspaceProps,
renderSidebarToggle,
detailPanelProps,
detailPanelOpen,
]);
const surface = useMemo<WorkspaceSurface>(() => {
if (showPreviewWorkspace) {
return previewSurface;
}
if (viewMode === 'desk') {
return workspaceSurface;
}
return documentsSurface;
}, [showPreviewWorkspace, viewMode, previewSurface, workspaceSurface, documentsSurface]);
}, [showPreviewWorkspace, previewSurface, documentsSurface]);
return { surface };
};
-22
View File
@@ -130,8 +130,6 @@ interface DesktopWorkspaceProps {
activeTagIds?: Array<Identifier | null>;
selectedDocumentIds?: Identifier[];
onClearSelection?: () => void;
detailPanelOpen?: boolean;
onCloseDetailPanel?: () => void;
tenantId?: Identifier | null;
viewId?: string | null;
}
@@ -175,8 +173,6 @@ interface DesktopWorkspaceViewProps {
onPromoteSelection?: DesktopWorkspaceProps['onPromoteSelection'];
selectedDocumentIds: Identifier[];
onClearSelection?: DesktopWorkspaceProps['onClearSelection'];
detailPanelOpen: boolean;
onCloseDetailPanel?: DesktopWorkspaceProps['onCloseDetailPanel'];
documentLookup: Map<string, DeskDocument>;
resolveBaseMetrics: (doc: DeskDocument | null, cardWidth: number, cardHeight: number) => {
baseWidth: number;
@@ -210,8 +206,6 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
activeTagIds = [],
selectedDocumentIds = [],
onClearSelection = null,
detailPanelOpen = false,
onCloseDetailPanel = null,
tenantId = null,
viewId = 'default',
previewEntries,
@@ -788,8 +782,6 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
onPromoteSelection,
selectedDocumentIds,
onClearSelection,
detailPanelOpen,
onCloseDetailPanel,
documentLookup,
resolveBaseMetrics,
bringToFront,
@@ -831,7 +823,6 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
layoutRef,
layoutSnapshot,
onClearSelection,
onCloseDetailPanel,
onDocumentStackSelect,
onEntryPointer,
onPromoteSelection,
@@ -847,7 +838,6 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
resolveBaseMetrics,
setDraggingId,
selectedDocumentIds,
detailPanelOpen,
onInspectDocument,
markLayoutDirty,
tagDropTargetId,
@@ -895,8 +885,6 @@ function DesktopWorkspaceView({
onPromoteSelection,
selectedDocumentIds,
onClearSelection,
detailPanelOpen,
onCloseDetailPanel,
documentLookup,
resolveBaseMetrics,
bringToFront,
@@ -951,8 +939,6 @@ function DesktopWorkspaceView({
onPromoteSelection,
onInspectDocument,
selectedDocumentIds,
detailPanelOpen,
onCloseDetailPanel,
openOverlayForDoc,
}) as {
getCardPointerHandlers: (doc: DeskDocument) => React.HTMLAttributes<HTMLDivElement>;
@@ -970,14 +956,6 @@ function DesktopWorkspaceView({
}
}, [focusShell, selectedDocumentIds.length]);
useEffect(() => {
if (!detailPanelOpen) {
focusShell();
}
}, [detailPanelOpen, focusShell]);
const allSizesReady = items.every((doc) => Boolean(ensureDocumentSize(doc)));
return (
@@ -1,124 +0,0 @@
import React, { ReactNode } from 'react';
import { SelectionFloatingPanel } from '../documents/SelectionFloatingActions';
import { createDocumentsTableHeaderActions } from '../documents/DocumentsPanel';
import createWorkspaceSurfaceConfig from '../documents/workspaceHeader';
import DocumentViewerPanel from '../preview/DocumentViewerPanel';
import DesktopWorkspace from './DesktopWorkspace';
type WorkspaceProps = Record<string, any>;
interface DetailProps extends Record<string, any> {
onClose?: () => void;
onOpenPreview?: () => void;
tags?: unknown;
}
interface CreateDesktopSurfaceArgs {
workspaceProps?: WorkspaceProps | null;
renderSidebarToggle?: () => ReactNode;
detailProps?: DetailProps | null;
detailOpen?: boolean;
}
const createDesktopSurface = ({
workspaceProps,
renderSidebarToggle,
detailProps = null,
detailOpen = false,
}: CreateDesktopSurfaceArgs) => {
if (!workspaceProps) {
return null;
}
const {
currentFolderName,
searchResults,
onRefresh,
viewMode,
onViewModeChange,
onDeleteSelection,
tags,
correspondents,
documentLookup,
tagLookupById,
onBulkTagAdd,
onBulkTagRemove,
onBulkCorrespondentAdd,
onBulkCorrespondentRemove,
onBulkReanalyze,
folderOptions,
onMoveDocumentsToFolder,
searchIncludeDescendants,
onToggleSearchIncludeDescendants,
} = workspaceProps;
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: viewMode || 'desk',
onViewModeChange,
onRefresh,
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 surfaceConfig = createWorkspaceSurfaceConfig({
key: 'workspace',
variant: 'workspace',
title,
subtitle,
sidebarToggle,
actions,
breadcrumbs: workspaceProps?.breadcrumbs || null,
selectionLabel: null,
floatingActions,
content: (
<DesktopWorkspace
{...workspaceProps}
/>
),
detail,
});
return {
...surfaceConfig,
supportsDetail: Boolean(detailProps),
};
};
export default createDesktopSurface;
@@ -37,8 +37,6 @@ export const useDeskPointer = ({
onPromoteSelection,
onInspectDocument,
selectedDocumentIds,
detailPanelOpen,
onCloseDetailPanel,
openOverlayForDoc = null,
}) => {
const pointerIntentRef = useRef(null);
@@ -404,12 +402,8 @@ export const useDeskPointer = ({
return;
}
if (detailPanelOpen) {
event.preventDefault();
safeInvoke(onCloseDetailPanel);
}
},
[detailPanelOpen, onCloseDetailPanel, openOverlayForDoc, selectedDocumentIds],
[openOverlayForDoc, selectedDocumentIds],
);
return {
@@ -1,255 +0,0 @@
import { useCallback, useMemo } from 'react';
type Identifier = string | number;
type DocumentEntry = { id?: Identifier } & Record<string, unknown>;
type InspectTarget = DocumentEntry | Identifier | null;
type WorkspaceViewMode = 'desk' | 'grid' | 'list' | string;
type DeskDocumentStackSelectHandler = (docIds: Identifier[]) => void;
type ResolveRowKey = (id: Identifier) => Identifier | string | null;
type ApplySelection = (
keys: Array<Identifier | string>,
options: { anchor?: Identifier | string | null; interactedKeys?: Array<Identifier | string> },
) => void;
interface UseDeskWorkspacePropsArgs {
documents?: DocumentEntry[];
searchResults?: DocumentEntry[];
breadcrumbs?: unknown[];
currentFolderName?: string | null;
documentsViewMode?: WorkspaceViewMode;
handleDocumentsViewModeChange?: (mode: WorkspaceViewMode) => void;
handleDeskExit?: () => void;
refreshCurrentFolder?: () => Promise<void> | void;
inspectDocument?: (doc: InspectTarget) => void;
handleEntryPointerCore?: (...args: unknown[]) => void;
promoteSelectionOrder?: (...args: unknown[]) => void;
currentTenantId?: Identifier | null;
selectedDocumentIds?: Identifier[];
selectedFolderIds?: Identifier[];
clearDocumentSelection?: () => void;
detailPanelOpen?: boolean;
handleDetailPanelClose?: () => void;
resolveThumbnailUrlForDoc?: (doc: DocumentEntry) => string | null;
handleDocumentTagDrop?: (...args: unknown[]) => void;
ensureAssetUrl?: (...args: unknown[]) => void;
getDocumentAsset?: (...args: unknown[]) => unknown;
activeTagFilters?: Identifier[];
handleDeleteSelection?: () => void;
tags?: unknown[];
correspondents?: unknown[];
documentLookup?: unknown;
tagLookupById?: unknown;
handleBulkTagAddFromDetail?: (...args: unknown[]) => void;
handleBulkTagRemoveFromDetail?: (...args: unknown[]) => void;
handleBulkCorrespondentAdd?: (...args: unknown[]) => void;
handleBulkCorrespondentRemove?: (...args: unknown[]) => void;
handleBulkSelectionReanalyze?: (...args: unknown[]) => void;
folderOptions?: unknown[];
moveDocumentsToFolder?: (...args: unknown[]) => void;
searchIncludeDescendants?: boolean;
toggleSearchIncludeDescendants?: () => void;
selectedEntries?: Array<Identifier | string>;
selectionAnchorRef: { current: Identifier | string | null };
applySelection: ApplySelection;
resolveDocumentRowKey: ResolveRowKey;
showingSearchResults?: boolean;
searchQuery?: string;
activeCorrespondentFilters?: Identifier[];
selectedFolder?: Identifier | string | null;
previewEntries?: Map<Identifier, { url?: string | null; contentType?: string | null }>;
ensureDownloadUrl?: (documentId: Identifier, options?: { force?: boolean }) => Promise<{ url?: string | null; contentType?: string | null } | null>;
}
const useDeskWorkspaceProps = ({
documents,
searchResults,
breadcrumbs,
currentFolderName,
documentsViewMode = 'desk',
handleDocumentsViewModeChange,
handleDeskExit,
refreshCurrentFolder,
inspectDocument,
handleEntryPointerCore,
promoteSelectionOrder,
currentTenantId,
selectedDocumentIds,
selectedFolderIds,
clearDocumentSelection,
detailPanelOpen,
handleDetailPanelClose,
resolveThumbnailUrlForDoc,
handleDocumentTagDrop,
ensureAssetUrl,
getDocumentAsset,
activeTagFilters = [],
handleDeleteSelection,
tags,
correspondents,
documentLookup,
tagLookupById,
handleBulkTagAddFromDetail,
handleBulkTagRemoveFromDetail,
handleBulkCorrespondentAdd,
handleBulkCorrespondentRemove,
handleBulkSelectionReanalyze,
folderOptions,
moveDocumentsToFolder,
searchIncludeDescendants,
toggleSearchIncludeDescendants,
selectedEntries = [],
selectionAnchorRef,
applySelection,
resolveDocumentRowKey,
showingSearchResults = false,
searchQuery = '',
activeCorrespondentFilters = [],
selectedFolder,
previewEntries,
ensureDownloadUrl,
}: UseDeskWorkspacePropsArgs) => {
const handleDeskDocumentStackSelect: DeskDocumentStackSelectHandler = useCallback(
(docIds) => {
if (!Array.isArray(docIds) || docIds.length === 0) {
return;
}
const rowKeys = docIds
.map((id) => resolveDocumentRowKey(id))
.filter((value): value is Identifier | string => Boolean(value));
if (!rowKeys.length) {
return;
}
const nextKeys = [...selectedEntries];
rowKeys.forEach((key) => {
if (!nextKeys.includes(key)) {
nextKeys.push(key);
}
});
const anchor = (rowKeys[0]
|| selectionAnchorRef.current
|| nextKeys[nextKeys.length - 1]) as Identifier | string | null;
applySelection(nextKeys, {
anchor,
interactedKeys: rowKeys,
});
},
[applySelection, resolveDocumentRowKey, selectedEntries, selectionAnchorRef],
);
const deskViewId = useMemo(() => {
if (showingSearchResults) {
const trimmedQuery = searchQuery.trim();
const tagsKey = [...activeTagFilters].sort().join(',');
const correspondentsKey = [...activeCorrespondentFilters].sort().join(',');
return `search:${trimmedQuery}|tags:${tagsKey}|corr:${correspondentsKey}`;
}
const folderKey = selectedFolder && selectedFolder !== '' ? selectedFolder : 'root';
return `folder:${folderKey}`;
}, [
showingSearchResults,
searchQuery,
activeTagFilters,
activeCorrespondentFilters,
selectedFolder,
]);
return useMemo(
() => ({
documents,
searchResults,
breadcrumbs,
currentFolderName,
viewMode: documentsViewMode,
onViewModeChange: handleDocumentsViewModeChange,
onExit: handleDeskExit,
onRefresh: refreshCurrentFolder,
onInspectDocument: inspectDocument,
onEntryPointer: handleEntryPointerCore,
onDocumentStackSelect: handleDeskDocumentStackSelect,
onPromoteSelection: promoteSelectionOrder,
tenantId: currentTenantId,
viewId: deskViewId,
selectedDocumentIds,
selectedFolderIds,
onClearSelection: clearDocumentSelection,
detailPanelOpen,
onCloseDetailPanel: handleDetailPanelClose,
resolveThumbnailUrl: resolveThumbnailUrlForDoc,
onAssignTagToDocument: handleDocumentTagDrop,
ensureAssetUrl,
getDocumentAsset,
activeTagIds: activeTagFilters,
onDeleteSelection: handleDeleteSelection,
tags,
correspondents,
documentLookup,
tagLookupById,
onBulkTagAdd: handleBulkTagAddFromDetail,
onBulkTagRemove: handleBulkTagRemoveFromDetail,
onBulkCorrespondentAdd: handleBulkCorrespondentAdd,
onBulkCorrespondentRemove: handleBulkCorrespondentRemove,
onBulkReanalyze: handleBulkSelectionReanalyze,
folderOptions,
onMoveDocumentsToFolder: moveDocumentsToFolder,
searchIncludeDescendants,
onToggleSearchIncludeDescendants: toggleSearchIncludeDescendants,
previewEntries,
ensureDownloadUrl,
}),
[
documents,
searchResults,
breadcrumbs,
currentFolderName,
documentsViewMode,
handleDocumentsViewModeChange,
handleDeskExit,
refreshCurrentFolder,
inspectDocument,
handleEntryPointerCore,
handleDeskDocumentStackSelect,
promoteSelectionOrder,
currentTenantId,
deskViewId,
selectedDocumentIds,
selectedFolderIds,
clearDocumentSelection,
detailPanelOpen,
handleDetailPanelClose,
resolveThumbnailUrlForDoc,
handleDocumentTagDrop,
ensureAssetUrl,
getDocumentAsset,
activeTagFilters,
handleDeleteSelection,
tags,
correspondents,
documentLookup,
tagLookupById,
handleBulkTagAddFromDetail,
handleBulkTagRemoveFromDetail,
handleBulkCorrespondentAdd,
handleBulkCorrespondentRemove,
handleBulkSelectionReanalyze,
folderOptions,
moveDocumentsToFolder,
searchIncludeDescendants,
toggleSearchIncludeDescendants,
previewEntries,
ensureDownloadUrl,
],
);
};
export default useDeskWorkspaceProps;
+134 -178
View File
@@ -1,8 +1,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ViewListIcon, ViewGridIcon, IconFileStack } from '../../ui/icons';
import BreadcrumbTrail from '../../ui/BreadcrumbTrail';
import DocumentsGrid from '../DocumentsGrid';
import DocumentsList from '../DocumentsList';
import DesktopWorkspace from '../../desktop/DesktopWorkspace';
import { isTagTransferEvent } from '../tagTransfer';
import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay';
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../useEntryPointer';
@@ -24,9 +23,8 @@ const defaultGetDocumentAsset = (_doc?: unknown, _type?: string) => null;
export type PreviewEntryLike = { url?: string | null; contentType?: string | null };
const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
currentFolderName,
currentFolderName: _currentFolderName,
breadcrumbs,
onRefresh,
subfolders,
documents,
searchResults,
@@ -54,10 +52,10 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
isSearchLoading = false,
onDocumentTagDrop,
viewMode = 'list',
onViewModeChange,
showHeader = true,
onViewModeChange: _onViewModeChange,
previewEntries,
ensureDownloadUrl,
deskWorkspaceProps = null,
}) => {
const {
selectedEntries,
@@ -403,20 +401,7 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
],
);
const isListView = viewMode === 'list';
const gridIconSize = DEFAULT_GRID_ICON_SIZE;
const handleSetViewMode = useCallback(
(nextMode) => {
if (!onViewModeChange) {
return;
}
onViewModeChange(nextMode);
if (scrollRef.current) {
scrollRef.current.scrollTop = 0;
}
},
[onViewModeChange],
);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = 0;
@@ -590,181 +575,152 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
const showDefaultEmptyState = !showingSearchResults && !isFilterActive && entries.length === 0;
const showListSearchEmptyState = showingSearchResults && !hasDocumentEntries && !isGridView && !isSearchLoading;
const showGridSearchEmptyState = isGridView && showingSearchResults && !hasDocumentEntries && !isSearchLoading;
const breadcrumbEntries = useMemo(() => (Array.isArray(breadcrumbs) ? breadcrumbs.filter(Boolean) : []), [breadcrumbs]);
const trailEntries = useMemo(() => {
if (!breadcrumbEntries.length) {
return [{ id: 'current-folder', label: currentFolderName }];
}
const lastIndex = breadcrumbEntries.length - 1;
return breadcrumbEntries.map((crumb, index) => ({
id: crumb.id ?? index,
label: crumb.name ?? crumb.label ?? crumb.title ?? '',
onClick: index < lastIndex && onFolderSelect
? () => onFolderSelect(crumb.id)
: null,
}));
}, [breadcrumbEntries, currentFolderName, onFolderSelect]);
return (
<>
<section
className={`documents-panel documents-panel--view-${isGridView ? 'grid' : 'list'}`}
>
{showHeader ? (
<div className="panel-section__header">
<div className="panel-section__titles">
<h2 className="documents-panel__title">
<BreadcrumbTrail
entries={trailEntries}
className="documents-panel__breadcrumbs"
separator="/"
/>
</h2>
{showingSearchResults && (
<div className="panel-section__subtitle">Search results</div>
)}
</div>
<div className="header-actions">
<div className="view-toggle" role="group" aria-label="Change view">
<button
type="button"
className={`view-toggle__button${isListView ? ' active' : ''}`}
onClick={() => handleSetViewMode('list')}
aria-pressed={isListView}
title="List view"
>
<ViewListIcon className="view-toggle__icon" size={18} />
</button>
<button
type="button"
className={`view-toggle__button${isGridView ? ' active' : ''}`}
onClick={() => handleSetViewMode('grid')}
aria-pressed={isGridView}
title="Icons view"
>
<ViewGridIcon className="view-toggle__icon" size={18} />
</button>
<button
type="button"
className={`view-toggle__button${isDeskView ? ' active' : ''}`}
onClick={() => handleSetViewMode('desk')}
aria-pressed={isDeskView}
title="Desk view"
>
<IconFileStack className="view-toggle__icon" size={18} />
</button>
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>
<button className="secondary" onClick={onRefresh}>
Refresh
</button>
</div>
)}
</div>
) : null}
{showDefaultEmptyState ? (
);
}
if (showDefaultEmptyState) {
return (
<div className="panel-section__body">
<div className="empty-state">
Drop files anywhere or onto a folder to upload documents.
</div>
</div>
) : showGridSearchEmptyState ? (
);
}
if (showGridSearchEmptyState) {
return (
<div className="panel-section__body">
<div className="empty-state empty-state--global">
No documents match the current filters.
</div>
</div>
) : showListSearchEmptyState ? (
);
}
if (showListSearchEmptyState) {
return (
<div className="panel-section__body">
<div className="empty-state">No documents match the current filters.</div>
</div>
) : (
<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}
/>
) : !showTableRows ? null : (
<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>
);
}
if (!showTableRows) {
return null;
}
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>
)}
</section>
</div>
);
};
const panelVariant = isDeskView ? 'desk' : isGridView ? 'grid' : 'list';
return (
<>
<section
className={`documents-panel documents-panel--view-${panelVariant}`}
>
{renderBody()}
</section>
<PreviewZoomOverlay
open={Boolean(zoomDisplay?.url)}
onClose={closePreviewOverlay}
@@ -47,6 +47,8 @@ const createDocumentsSurface = ({
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'}`
@@ -99,8 +101,8 @@ const createDocumentsSurface = ({
: null;
return createWorkspaceSurfaceConfig({
key: 'documents',
variant: 'documents',
key: isDeskView ? 'workspace' : 'documents',
variant: isDeskView ? 'workspace' : 'documents',
title,
subtitle,
sidebarToggle,
@@ -111,7 +113,6 @@ const createDocumentsSurface = ({
content: (
<DocumentsPanel
{...tableProps}
showHeader={false}
onInspectDocument={onInspectDocument}
/>
),
@@ -18,7 +18,6 @@ import useDocumentsSelection from '../../documents/hooks/useDocumentsSelection';
import useBulkDocumentActions from '../../documents/hooks/useBulkDocumentActions';
import useDocumentsPanelProps from '../../documents/hooks/useDocumentsPanelProps';
import useDocumentPreview from '../../app/useDocumentPreview';
import useDeskWorkspaceProps from '../../desktop/useDeskWorkspaceProps';
import useSidebarProps from '../../sidebar/useSidebarProps';
import {
ASSET_PRESIGN_TTL_MS,
@@ -97,7 +96,6 @@ interface UseDocumentsWorkspaceOptions {
onToggleSearchIncludeDescendants?: () => void;
onSetSearchIncludeDescendants?: (value: boolean) => void;
sortRefreshReadyRef?: MutableRefObject<boolean>;
handleDeskExit?: () => void;
}
const useDocumentsWorkspace = ({
@@ -113,14 +111,12 @@ const useDocumentsWorkspace = ({
onToggleSearchIncludeDescendants,
onSetSearchIncludeDescendants,
sortRefreshReadyRef,
handleDeskExit,
}: UseDocumentsWorkspaceOptions = {}) => {
const handleDocumentsViewModeChange = onDocumentsViewModeChange || noop;
const handleDocumentsSortFieldChange = onDocumentsSortFieldChange || noop;
const handleDocumentsSortDirectionToggle = onDocumentsSortDirectionToggle || noop;
const toggleSearchIncludeDescendants = onToggleSearchIncludeDescendants || noop;
const setSearchIncludeDescendants = onSetSearchIncludeDescendants || noop;
const handleDeskExitSafe = handleDeskExit || noop;
const fallbackSortFieldRef = useRef(documentsSortField);
const activeSortFieldRef = documentsSortFieldRef || fallbackSortFieldRef;
@@ -1236,12 +1232,10 @@ const useDocumentsWorkspace = ({
detailPanelProps,
detailPanelOpen,
openDetailPanel,
handleDetailPanelClose,
inspectDocument,
previewActive,
previewWorkspaceDocument,
previewWorkspaceEntry,
resolveThumbnailUrlForDoc,
resolveFolderPath,
} = useDetailWorkspace({
documents,
@@ -1404,7 +1398,97 @@ const useDocumentsWorkspace = ({
});
const documentsTableProps = useDocumentsPanelProps({
const handleDeskDocumentStackSelect = useCallback(
(docIds: Array<Identifier | string>) => {
if (!Array.isArray(docIds) || docIds.length === 0) {
return;
}
const rowKeys = docIds
.map((id) => resolveDocumentRowKey(id as Identifier))
.filter((value): value is string => typeof value === 'string');
if (!rowKeys.length) {
return;
}
const nextKeys = [...selectedEntries];
rowKeys.forEach((key) => {
if (!nextKeys.includes(key)) {
nextKeys.push(key);
}
});
const anchor = (rowKeys[0]
|| selectionAnchorRef.current
|| nextKeys[nextKeys.length - 1]) as Identifier | string | null;
applySelection(nextKeys, {
anchor,
interactedKeys: rowKeys,
});
},
[applySelection, selectedEntries, selectionAnchorRef],
);
const deskViewId = useMemo(() => {
if (showingSearchResults) {
const trimmedQuery = searchQuery.trim();
const tagsKey = [...activeTagFilters].sort().join(',');
const correspondentsKey = [...activeCorrespondentFilters].sort().join(',');
return `search:${trimmedQuery}|tags:${tagsKey}|corr:${correspondentsKey}`;
}
const folderKey = selectedFolder && selectedFolder !== '' ? selectedFolder : 'root';
return `folder:${folderKey}`;
}, [
showingSearchResults,
searchQuery,
activeTagFilters,
activeCorrespondentFilters,
selectedFolder,
]);
const deskWorkspaceProps = useMemo(
() => ({
documents,
searchResults,
onInspectDocument: inspectDocumentForDesk,
onEntryPointer: handleEntryPointerCore,
onDocumentStackSelect: handleDeskDocumentStackSelect,
onPromoteSelection: promoteSelectionOrder,
onAssignTagToDocument: handleDocumentTagDrop,
ensureAssetUrl,
getDocumentAsset,
activeTagIds: activeTagFilters,
selectedDocumentIds,
onClearSelection: clearDocumentSelection,
tenantId: currentTenantId,
viewId: deskViewId,
previewEntries,
ensureDownloadUrl,
}),
[
documents,
searchResults,
inspectDocumentForDesk,
handleEntryPointerCore,
handleDeskDocumentStackSelect,
promoteSelectionOrder,
handleDocumentTagDrop,
ensureAssetUrl,
getDocumentAsset,
activeTagFilters,
selectedDocumentIds,
clearDocumentSelection,
currentTenantId,
deskViewId,
previewEntries,
ensureDownloadUrl,
],
);
const documentsPanelProps = useDocumentsPanelProps({
currentFolderName,
breadcrumbs,
refreshCurrentFolder,
@@ -1457,6 +1541,14 @@ const useDocumentsWorkspace = ({
ensureDownloadUrl,
});
const documentsTableProps = useMemo(
() => ({
...documentsPanelProps,
deskWorkspaceProps,
}),
[documentsPanelProps, deskWorkspaceProps],
);
const sidebarProps = useSidebarProps({
folderNodes,
folderClickHandlers,
@@ -1495,57 +1587,6 @@ const useDocumentsWorkspace = ({
uploadQueue,
});
const deskWorkspaceProps = useDeskWorkspaceProps({
documents,
searchResults,
breadcrumbs,
currentFolderName,
documentsViewMode,
handleDocumentsViewModeChange,
handleDeskExit: handleDeskExitSafe,
refreshCurrentFolder,
inspectDocument: inspectDocumentForDesk,
handleEntryPointerCore,
promoteSelectionOrder,
currentTenantId,
selectedDocumentIds,
selectedFolderIds,
clearDocumentSelection,
detailPanelOpen,
handleDetailPanelClose,
resolveThumbnailUrlForDoc,
handleDocumentTagDrop,
ensureAssetUrl,
getDocumentAsset,
activeTagFilters,
handleDeleteSelection,
tags,
correspondents,
documentLookup,
tagLookupById,
handleBulkTagAddFromDetail,
handleBulkTagRemoveFromDetail,
handleBulkCorrespondentAdd,
handleBulkCorrespondentRemove,
handleBulkSelectionReanalyze,
folderOptions,
moveDocumentsToFolder,
searchIncludeDescendants,
toggleSearchIncludeDescendants,
selectedEntries,
selectionAnchorRef,
applySelection,
resolveDocumentRowKey,
showingSearchResults,
searchQuery,
activeCorrespondentFilters,
selectedFolder,
previewEntries,
ensureDownloadUrl,
});
const contextValue = useMemo(
() => ({
token,
@@ -1585,7 +1626,6 @@ const useDocumentsWorkspace = ({
documentsTableProps,
detailPanelProps,
documentsViewMode,
deskWorkspaceProps,
ensurePreviewData,
ensureAssetUrl,
resolveFolderPath,
@@ -1637,7 +1677,6 @@ const useDocumentsWorkspace = ({
documentsTableProps,
detailPanelProps,
documentsViewMode,
deskWorkspaceProps,
ensurePreviewData,
ensureAssetUrl,
resolveFolderPath,
+13 -17
View File
@@ -93,6 +93,19 @@ const srgbChannelToLinear = (value: number) => {
return ((normalized + 0.055) / 1.055) ** 2.4;
};
export const hexToRgb = (input?: string): (RgbColor & { hex: string }) | null => {
if (!input) return null;
const match = HEX_COLOR_PATTERN.exec(input.trim());
if (!match) return null;
const value = parseInt(match[1], 16);
return {
r: (value >> 16) & 0xff,
g: (value >> 8) & 0xff,
b: value & 0xff,
hex: `#${match[1].toLowerCase()}`,
};
};
const hexToOklch = (input?: string) => {
const rgb = hexToRgb(input);
if (!rgb) {
@@ -150,19 +163,6 @@ const buildTagStyle = (backgroundHex: string) => {
};
};
export const hexToRgb = (input?: string): (RgbColor & { hex: string }) | null => {
if (!input) return null;
const match = HEX_COLOR_PATTERN.exec(input.trim());
if (!match) return null;
const value = parseInt(match[1], 16);
return {
r: (value >> 16) & 0xff,
g: (value >> 8) & 0xff,
b: value & 0xff,
hex: `#${match[1].toLowerCase()}`,
};
};
export const relativeLuminance = ({ r, g, b }: RgbColor): number => {
const toLinear = (channel) => {
const normalized = channel / 255;
@@ -295,7 +295,6 @@ export const getTagColorStyle = (hex) => {
adjustments.forEach((delta) => registerCandidate(baseHsl.l + delta));
let bestBackground = baseHex;
let bestText = baseText;
let bestRatio = baseRatio;
if (bestRatio >= TARGET_RATIO) {
@@ -303,7 +302,6 @@ export const getTagColorStyle = (hex) => {
}
let compliantBackground = null;
let compliantText = null;
let compliantDelta = Infinity;
let compliantRatio = -Infinity;
@@ -314,7 +312,6 @@ export const getTagColorStyle = (hex) => {
if (ratio > bestRatio) {
bestBackground = candidateHex;
bestText = textHex;
bestRatio = ratio;
}
@@ -330,7 +327,6 @@ export const getTagColorStyle = (hex) => {
|| (isSimilarDistance && improvesRatio)
) {
compliantBackground = candidateHex;
compliantText = textHex;
compliantDelta = delta;
compliantRatio = ratio;
}