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, onToggleSearchIncludeDescendants: documentsPreferences.toggleSearchIncludeDescendants,
onSetSearchIncludeDescendants: documentsPreferences.setSearchIncludeDescendants, onSetSearchIncludeDescendants: documentsPreferences.setSearchIncludeDescendants,
sortRefreshReadyRef: documentsPreferences.sortRefreshReadyRef, sortRefreshReadyRef: documentsPreferences.sortRefreshReadyRef,
handleDeskExit: documentsPreferences.handleDeskExit,
}); });
if (['logged-out', 'authenticating', 'selecting-tenant'].includes(appStatus)) { if (['logged-out', 'authenticating', 'selecting-tenant'].includes(appStatus)) {
-6
View File
@@ -34,8 +34,6 @@ interface DocumentsRouteAppShell {
documentsTableProps?: DocumentsTableProps | null; documentsTableProps?: DocumentsTableProps | null;
detailPanelProps?: Record<string, unknown> | null; detailPanelProps?: Record<string, unknown> | null;
detailPanelOpen?: boolean; detailPanelOpen?: boolean;
documentsViewMode?: string;
deskWorkspaceProps?: Record<string, unknown> | null;
openTagsModal?: () => void; openTagsModal?: () => void;
openCorrespondentsModal?: () => void; openCorrespondentsModal?: () => void;
previewWorkspaceDocument?: unknown; previewWorkspaceDocument?: unknown;
@@ -55,8 +53,6 @@ const DocumentsRouteContent: React.FC = () => {
documentsTableProps, documentsTableProps,
detailPanelProps, detailPanelProps,
detailPanelOpen, detailPanelOpen,
documentsViewMode,
deskWorkspaceProps,
openTagsModal, openTagsModal,
openCorrespondentsModal, openCorrespondentsModal,
previewWorkspaceDocument, previewWorkspaceDocument,
@@ -106,8 +102,6 @@ const DocumentsRouteContent: React.FC = () => {
documentsTableProps, documentsTableProps,
detailPanelProps, detailPanelProps,
detailPanelOpen, detailPanelOpen,
viewMode: documentsViewMode,
deskWorkspaceProps,
previewWorkspaceDocument, previewWorkspaceDocument,
previewWorkspaceEntry, previewWorkspaceEntry,
previewDocumentId, previewDocumentId,
+2 -29
View File
@@ -3,7 +3,6 @@ import type { ReactNode } from 'react';
import { SidebarExpandIcon } from '../ui/icons'; import { SidebarExpandIcon } from '../ui/icons';
import { createDocumentsSurface } from '../documents/DocumentsPanel'; import { createDocumentsSurface } from '../documents/DocumentsPanel';
import { createDocumentViewerSurface } from '../preview/DocumentViewerPanel'; import { createDocumentViewerSurface } from '../preview/DocumentViewerPanel';
import createDesktopSurface from '../desktop/createDesktopSurface';
import { usePanelManager } from './PanelManagerContext'; import { usePanelManager } from './PanelManagerContext';
type Identifier = string | number; type Identifier = string | number;
@@ -16,8 +15,7 @@ type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
type DocumentsSurface = ReturnType<typeof createDocumentsSurface> | null; type DocumentsSurface = ReturnType<typeof createDocumentsSurface> | null;
type PreviewSurface = ReturnType<typeof createDocumentViewerSurface> | null; type PreviewSurface = ReturnType<typeof createDocumentViewerSurface> | null;
type DesktopSurface = ReturnType<typeof createDesktopSurface> | null; type WorkspaceSurface = DocumentsSurface | PreviewSurface | null;
type WorkspaceSurface = DocumentsSurface | PreviewSurface | DesktopSurface | null;
interface UseWorkspaceSurfaceArgs { interface UseWorkspaceSurfaceArgs {
sidebarHidden?: boolean; sidebarHidden?: boolean;
@@ -25,8 +23,6 @@ interface UseWorkspaceSurfaceArgs {
documentsTableProps?: Record<string, any> | null; documentsTableProps?: Record<string, any> | null;
detailPanelProps?: (Record<string, any> & { onClose?: () => void }) | null; detailPanelProps?: (Record<string, any> & { onClose?: () => void }) | null;
detailPanelOpen?: boolean; detailPanelOpen?: boolean;
viewMode?: string;
deskWorkspaceProps?: Record<string, any> | null;
previewWorkspaceDocument?: unknown; previewWorkspaceDocument?: unknown;
previewWorkspaceEntry?: unknown; previewWorkspaceEntry?: unknown;
previewDocumentId?: Identifier | null; previewDocumentId?: Identifier | null;
@@ -48,8 +44,6 @@ export const useWorkspaceSurface = ({
documentsTableProps, documentsTableProps,
detailPanelProps, detailPanelProps,
detailPanelOpen = false, detailPanelOpen = false,
viewMode = 'list',
deskWorkspaceProps,
previewWorkspaceDocument, previewWorkspaceDocument,
previewWorkspaceEntry, previewWorkspaceEntry,
previewDocumentId, previewDocumentId,
@@ -161,33 +155,12 @@ export const useWorkspaceSurface = ({
detailPanelProps, 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>(() => { const surface = useMemo<WorkspaceSurface>(() => {
if (showPreviewWorkspace) { if (showPreviewWorkspace) {
return previewSurface; return previewSurface;
} }
if (viewMode === 'desk') {
return workspaceSurface;
}
return documentsSurface; return documentsSurface;
}, [showPreviewWorkspace, viewMode, previewSurface, workspaceSurface, documentsSurface]); }, [showPreviewWorkspace, previewSurface, documentsSurface]);
return { surface }; return { surface };
}; };
-22
View File
@@ -130,8 +130,6 @@ interface DesktopWorkspaceProps {
activeTagIds?: Array<Identifier | null>; activeTagIds?: Array<Identifier | null>;
selectedDocumentIds?: Identifier[]; selectedDocumentIds?: Identifier[];
onClearSelection?: () => void; onClearSelection?: () => void;
detailPanelOpen?: boolean;
onCloseDetailPanel?: () => void;
tenantId?: Identifier | null; tenantId?: Identifier | null;
viewId?: string | null; viewId?: string | null;
} }
@@ -175,8 +173,6 @@ interface DesktopWorkspaceViewProps {
onPromoteSelection?: DesktopWorkspaceProps['onPromoteSelection']; onPromoteSelection?: DesktopWorkspaceProps['onPromoteSelection'];
selectedDocumentIds: Identifier[]; selectedDocumentIds: Identifier[];
onClearSelection?: DesktopWorkspaceProps['onClearSelection']; onClearSelection?: DesktopWorkspaceProps['onClearSelection'];
detailPanelOpen: boolean;
onCloseDetailPanel?: DesktopWorkspaceProps['onCloseDetailPanel'];
documentLookup: Map<string, DeskDocument>; documentLookup: Map<string, DeskDocument>;
resolveBaseMetrics: (doc: DeskDocument | null, cardWidth: number, cardHeight: number) => { resolveBaseMetrics: (doc: DeskDocument | null, cardWidth: number, cardHeight: number) => {
baseWidth: number; baseWidth: number;
@@ -210,8 +206,6 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
activeTagIds = [], activeTagIds = [],
selectedDocumentIds = [], selectedDocumentIds = [],
onClearSelection = null, onClearSelection = null,
detailPanelOpen = false,
onCloseDetailPanel = null,
tenantId = null, tenantId = null,
viewId = 'default', viewId = 'default',
previewEntries, previewEntries,
@@ -788,8 +782,6 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
onPromoteSelection, onPromoteSelection,
selectedDocumentIds, selectedDocumentIds,
onClearSelection, onClearSelection,
detailPanelOpen,
onCloseDetailPanel,
documentLookup, documentLookup,
resolveBaseMetrics, resolveBaseMetrics,
bringToFront, bringToFront,
@@ -831,7 +823,6 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
layoutRef, layoutRef,
layoutSnapshot, layoutSnapshot,
onClearSelection, onClearSelection,
onCloseDetailPanel,
onDocumentStackSelect, onDocumentStackSelect,
onEntryPointer, onEntryPointer,
onPromoteSelection, onPromoteSelection,
@@ -847,7 +838,6 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
resolveBaseMetrics, resolveBaseMetrics,
setDraggingId, setDraggingId,
selectedDocumentIds, selectedDocumentIds,
detailPanelOpen,
onInspectDocument, onInspectDocument,
markLayoutDirty, markLayoutDirty,
tagDropTargetId, tagDropTargetId,
@@ -895,8 +885,6 @@ function DesktopWorkspaceView({
onPromoteSelection, onPromoteSelection,
selectedDocumentIds, selectedDocumentIds,
onClearSelection, onClearSelection,
detailPanelOpen,
onCloseDetailPanel,
documentLookup, documentLookup,
resolveBaseMetrics, resolveBaseMetrics,
bringToFront, bringToFront,
@@ -951,8 +939,6 @@ function DesktopWorkspaceView({
onPromoteSelection, onPromoteSelection,
onInspectDocument, onInspectDocument,
selectedDocumentIds, selectedDocumentIds,
detailPanelOpen,
onCloseDetailPanel,
openOverlayForDoc, openOverlayForDoc,
}) as { }) as {
getCardPointerHandlers: (doc: DeskDocument) => React.HTMLAttributes<HTMLDivElement>; getCardPointerHandlers: (doc: DeskDocument) => React.HTMLAttributes<HTMLDivElement>;
@@ -970,14 +956,6 @@ function DesktopWorkspaceView({
} }
}, [focusShell, selectedDocumentIds.length]); }, [focusShell, selectedDocumentIds.length]);
useEffect(() => {
if (!detailPanelOpen) {
focusShell();
}
}, [detailPanelOpen, focusShell]);
const allSizesReady = items.every((doc) => Boolean(ensureDocumentSize(doc))); const allSizesReady = items.every((doc) => Boolean(ensureDocumentSize(doc)));
return ( 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, onPromoteSelection,
onInspectDocument, onInspectDocument,
selectedDocumentIds, selectedDocumentIds,
detailPanelOpen,
onCloseDetailPanel,
openOverlayForDoc = null, openOverlayForDoc = null,
}) => { }) => {
const pointerIntentRef = useRef(null); const pointerIntentRef = useRef(null);
@@ -404,12 +402,8 @@ export const useDeskPointer = ({
return; return;
} }
if (detailPanelOpen) {
event.preventDefault();
safeInvoke(onCloseDetailPanel);
}
}, },
[detailPanelOpen, onCloseDetailPanel, openOverlayForDoc, selectedDocumentIds], [openOverlayForDoc, selectedDocumentIds],
); );
return { 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;
+48 -92
View File
@@ -1,8 +1,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 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 DocumentsGrid from '../DocumentsGrid';
import DocumentsList from '../DocumentsList'; import DocumentsList from '../DocumentsList';
import DesktopWorkspace from '../../desktop/DesktopWorkspace';
import { isTagTransferEvent } from '../tagTransfer'; import { isTagTransferEvent } from '../tagTransfer';
import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay'; import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay';
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../useEntryPointer'; 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 }; export type PreviewEntryLike = { url?: string | null; contentType?: string | null };
const DocumentsPanel: React.FC<DocumentsPanelProps> = ({ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
currentFolderName, currentFolderName: _currentFolderName,
breadcrumbs, breadcrumbs,
onRefresh,
subfolders, subfolders,
documents, documents,
searchResults, searchResults,
@@ -54,10 +52,10 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
isSearchLoading = false, isSearchLoading = false,
onDocumentTagDrop, onDocumentTagDrop,
viewMode = 'list', viewMode = 'list',
onViewModeChange, onViewModeChange: _onViewModeChange,
showHeader = true,
previewEntries, previewEntries,
ensureDownloadUrl, ensureDownloadUrl,
deskWorkspaceProps = null,
}) => { }) => {
const { const {
selectedEntries, selectedEntries,
@@ -403,20 +401,7 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
], ],
); );
const isListView = viewMode === 'list';
const gridIconSize = DEFAULT_GRID_ICON_SIZE; const gridIconSize = DEFAULT_GRID_ICON_SIZE;
const handleSetViewMode = useCallback(
(nextMode) => {
if (!onViewModeChange) {
return;
}
onViewModeChange(nextMode);
if (scrollRef.current) {
scrollRef.current.scrollTop = 0;
}
},
[onViewModeChange],
);
useEffect(() => { useEffect(() => {
if (scrollRef.current) { if (scrollRef.current) {
scrollRef.current.scrollTop = 0; scrollRef.current.scrollTop = 0;
@@ -590,93 +575,54 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
const showDefaultEmptyState = !showingSearchResults && !isFilterActive && entries.length === 0; const showDefaultEmptyState = !showingSearchResults && !isFilterActive && entries.length === 0;
const showListSearchEmptyState = showingSearchResults && !hasDocumentEntries && !isGridView && !isSearchLoading; const showListSearchEmptyState = showingSearchResults && !hasDocumentEntries && !isGridView && !isSearchLoading;
const showGridSearchEmptyState = isGridView && showingSearchResults && !hasDocumentEntries && !isSearchLoading; const showGridSearchEmptyState = isGridView && showingSearchResults && !hasDocumentEntries && !isSearchLoading;
const breadcrumbEntries = useMemo(() => (Array.isArray(breadcrumbs) ? breadcrumbs.filter(Boolean) : []), [breadcrumbs]); const renderBody = () => {
const trailEntries = useMemo(() => { if (isDeskView) {
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 ( return (
<> <div className="panel-section__body panel-section__body--desk">
<section {deskWorkspaceProps ? (
className={`documents-panel documents-panel--view-${isGridView ? 'grid' : 'list'}`} <DesktopWorkspace {...deskWorkspaceProps} />
> ) : (
{showHeader ? ( <div className="empty-state empty-state--global">
<div className="panel-section__header"> Desk view is unavailable.
<div className="panel-section__titles"> </div>
<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>
<div className="header-actions"> );
<div className="view-toggle" role="group" aria-label="Change view"> }
<button
type="button" if (showDefaultEmptyState) {
className={`view-toggle__button${isListView ? ' active' : ''}`} return (
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>
</div>
<button className="secondary" onClick={onRefresh}>
Refresh
</button>
</div>
</div>
) : null}
{showDefaultEmptyState ? (
<div className="panel-section__body"> <div className="panel-section__body">
<div className="empty-state"> <div className="empty-state">
Drop files anywhere or onto a folder to upload documents. Drop files anywhere or onto a folder to upload documents.
</div> </div>
</div> </div>
) : showGridSearchEmptyState ? ( );
}
if (showGridSearchEmptyState) {
return (
<div className="panel-section__body"> <div className="panel-section__body">
<div className="empty-state empty-state--global"> <div className="empty-state empty-state--global">
No documents match the current filters. No documents match the current filters.
</div> </div>
</div> </div>
) : showListSearchEmptyState ? ( );
}
if (showListSearchEmptyState) {
return (
<div className="panel-section__body"> <div className="panel-section__body">
<div className="empty-state">No documents match the current filters.</div> <div className="empty-state">No documents match the current filters.</div>
</div> </div>
) : ( );
}
if (!showTableRows) {
return null;
}
return (
<div className="panel-section__body"> <div className="panel-section__body">
<div <div
ref={assignScrollRef} ref={assignScrollRef}
@@ -730,7 +676,7 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
onDocumentRename={onDocumentRename} onDocumentRename={onDocumentRename}
onFolderRename={onFolderRename} onFolderRename={onFolderRename}
/> />
) : !showTableRows ? null : ( ) : (
<DocumentsList <DocumentsList
entries={entries} entries={entries}
focusedRowKey={focusedRowKey} focusedRowKey={focusedRowKey}
@@ -763,7 +709,17 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
)} )}
</div> </div>
</div> </div>
)} );
};
const panelVariant = isDeskView ? 'desk' : isGridView ? 'grid' : 'list';
return (
<>
<section
className={`documents-panel documents-panel--view-${panelVariant}`}
>
{renderBody()}
</section> </section>
<PreviewZoomOverlay <PreviewZoomOverlay
open={Boolean(zoomDisplay?.url)} open={Boolean(zoomDisplay?.url)}
@@ -47,6 +47,8 @@ const createDocumentsSurface = ({
onInspectDocument, onInspectDocument,
} = tableProps; } = tableProps;
const isDeskView = viewMode === 'desk';
const title = Array.isArray(searchResults) ? 'Search results' : currentFolderName; const title = Array.isArray(searchResults) ? 'Search results' : currentFolderName;
const subtitle = Array.isArray(searchResults) const subtitle = Array.isArray(searchResults)
? `${searchResults.length} matching document${searchResults.length === 1 ? '' : 's'}` ? `${searchResults.length} matching document${searchResults.length === 1 ? '' : 's'}`
@@ -99,8 +101,8 @@ const createDocumentsSurface = ({
: null; : null;
return createWorkspaceSurfaceConfig({ return createWorkspaceSurfaceConfig({
key: 'documents', key: isDeskView ? 'workspace' : 'documents',
variant: 'documents', variant: isDeskView ? 'workspace' : 'documents',
title, title,
subtitle, subtitle,
sidebarToggle, sidebarToggle,
@@ -111,7 +113,6 @@ const createDocumentsSurface = ({
content: ( content: (
<DocumentsPanel <DocumentsPanel
{...tableProps} {...tableProps}
showHeader={false}
onInspectDocument={onInspectDocument} onInspectDocument={onInspectDocument}
/> />
), ),
@@ -18,7 +18,6 @@ import useDocumentsSelection from '../../documents/hooks/useDocumentsSelection';
import useBulkDocumentActions from '../../documents/hooks/useBulkDocumentActions'; import useBulkDocumentActions from '../../documents/hooks/useBulkDocumentActions';
import useDocumentsPanelProps from '../../documents/hooks/useDocumentsPanelProps'; import useDocumentsPanelProps from '../../documents/hooks/useDocumentsPanelProps';
import useDocumentPreview from '../../app/useDocumentPreview'; import useDocumentPreview from '../../app/useDocumentPreview';
import useDeskWorkspaceProps from '../../desktop/useDeskWorkspaceProps';
import useSidebarProps from '../../sidebar/useSidebarProps'; import useSidebarProps from '../../sidebar/useSidebarProps';
import { import {
ASSET_PRESIGN_TTL_MS, ASSET_PRESIGN_TTL_MS,
@@ -97,7 +96,6 @@ interface UseDocumentsWorkspaceOptions {
onToggleSearchIncludeDescendants?: () => void; onToggleSearchIncludeDescendants?: () => void;
onSetSearchIncludeDescendants?: (value: boolean) => void; onSetSearchIncludeDescendants?: (value: boolean) => void;
sortRefreshReadyRef?: MutableRefObject<boolean>; sortRefreshReadyRef?: MutableRefObject<boolean>;
handleDeskExit?: () => void;
} }
const useDocumentsWorkspace = ({ const useDocumentsWorkspace = ({
@@ -113,14 +111,12 @@ const useDocumentsWorkspace = ({
onToggleSearchIncludeDescendants, onToggleSearchIncludeDescendants,
onSetSearchIncludeDescendants, onSetSearchIncludeDescendants,
sortRefreshReadyRef, sortRefreshReadyRef,
handleDeskExit,
}: UseDocumentsWorkspaceOptions = {}) => { }: UseDocumentsWorkspaceOptions = {}) => {
const handleDocumentsViewModeChange = onDocumentsViewModeChange || noop; const handleDocumentsViewModeChange = onDocumentsViewModeChange || noop;
const handleDocumentsSortFieldChange = onDocumentsSortFieldChange || noop; const handleDocumentsSortFieldChange = onDocumentsSortFieldChange || noop;
const handleDocumentsSortDirectionToggle = onDocumentsSortDirectionToggle || noop; const handleDocumentsSortDirectionToggle = onDocumentsSortDirectionToggle || noop;
const toggleSearchIncludeDescendants = onToggleSearchIncludeDescendants || noop; const toggleSearchIncludeDescendants = onToggleSearchIncludeDescendants || noop;
const setSearchIncludeDescendants = onSetSearchIncludeDescendants || noop; const setSearchIncludeDescendants = onSetSearchIncludeDescendants || noop;
const handleDeskExitSafe = handleDeskExit || noop;
const fallbackSortFieldRef = useRef(documentsSortField); const fallbackSortFieldRef = useRef(documentsSortField);
const activeSortFieldRef = documentsSortFieldRef || fallbackSortFieldRef; const activeSortFieldRef = documentsSortFieldRef || fallbackSortFieldRef;
@@ -1236,12 +1232,10 @@ const useDocumentsWorkspace = ({
detailPanelProps, detailPanelProps,
detailPanelOpen, detailPanelOpen,
openDetailPanel, openDetailPanel,
handleDetailPanelClose,
inspectDocument, inspectDocument,
previewActive, previewActive,
previewWorkspaceDocument, previewWorkspaceDocument,
previewWorkspaceEntry, previewWorkspaceEntry,
resolveThumbnailUrlForDoc,
resolveFolderPath, resolveFolderPath,
} = useDetailWorkspace({ } = useDetailWorkspace({
documents, 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, currentFolderName,
breadcrumbs, breadcrumbs,
refreshCurrentFolder, refreshCurrentFolder,
@@ -1457,6 +1541,14 @@ const useDocumentsWorkspace = ({
ensureDownloadUrl, ensureDownloadUrl,
}); });
const documentsTableProps = useMemo(
() => ({
...documentsPanelProps,
deskWorkspaceProps,
}),
[documentsPanelProps, deskWorkspaceProps],
);
const sidebarProps = useSidebarProps({ const sidebarProps = useSidebarProps({
folderNodes, folderNodes,
folderClickHandlers, folderClickHandlers,
@@ -1495,57 +1587,6 @@ const useDocumentsWorkspace = ({
uploadQueue, 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( const contextValue = useMemo(
() => ({ () => ({
token, token,
@@ -1585,7 +1626,6 @@ const useDocumentsWorkspace = ({
documentsTableProps, documentsTableProps,
detailPanelProps, detailPanelProps,
documentsViewMode, documentsViewMode,
deskWorkspaceProps,
ensurePreviewData, ensurePreviewData,
ensureAssetUrl, ensureAssetUrl,
resolveFolderPath, resolveFolderPath,
@@ -1637,7 +1677,6 @@ const useDocumentsWorkspace = ({
documentsTableProps, documentsTableProps,
detailPanelProps, detailPanelProps,
documentsViewMode, documentsViewMode,
deskWorkspaceProps,
ensurePreviewData, ensurePreviewData,
ensureAssetUrl, ensureAssetUrl,
resolveFolderPath, resolveFolderPath,
+13 -17
View File
@@ -93,6 +93,19 @@ const srgbChannelToLinear = (value: number) => {
return ((normalized + 0.055) / 1.055) ** 2.4; 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 hexToOklch = (input?: string) => {
const rgb = hexToRgb(input); const rgb = hexToRgb(input);
if (!rgb) { 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 => { export const relativeLuminance = ({ r, g, b }: RgbColor): number => {
const toLinear = (channel) => { const toLinear = (channel) => {
const normalized = channel / 255; const normalized = channel / 255;
@@ -295,7 +295,6 @@ export const getTagColorStyle = (hex) => {
adjustments.forEach((delta) => registerCandidate(baseHsl.l + delta)); adjustments.forEach((delta) => registerCandidate(baseHsl.l + delta));
let bestBackground = baseHex; let bestBackground = baseHex;
let bestText = baseText;
let bestRatio = baseRatio; let bestRatio = baseRatio;
if (bestRatio >= TARGET_RATIO) { if (bestRatio >= TARGET_RATIO) {
@@ -303,7 +302,6 @@ export const getTagColorStyle = (hex) => {
} }
let compliantBackground = null; let compliantBackground = null;
let compliantText = null;
let compliantDelta = Infinity; let compliantDelta = Infinity;
let compliantRatio = -Infinity; let compliantRatio = -Infinity;
@@ -314,7 +312,6 @@ export const getTagColorStyle = (hex) => {
if (ratio > bestRatio) { if (ratio > bestRatio) {
bestBackground = candidateHex; bestBackground = candidateHex;
bestText = textHex;
bestRatio = ratio; bestRatio = ratio;
} }
@@ -330,7 +327,6 @@ export const getTagColorStyle = (hex) => {
|| (isSimilarDistance && improvesRatio) || (isSimilarDistance && improvesRatio)
) { ) {
compliantBackground = candidateHex; compliantBackground = candidateHex;
compliantText = textHex;
compliantDelta = delta; compliantDelta = delta;
compliantRatio = ratio; compliantRatio = ratio;
} }