diff --git a/frontend/src/app/AppLayout.tsx b/frontend/src/app/AppLayout.tsx index 8fdd324..35b94c6 100644 --- a/frontend/src/app/AppLayout.tsx +++ b/frontend/src/app/AppLayout.tsx @@ -6,6 +6,7 @@ import UploadQueueOverlay from './UploadQueueOverlay'; import useDocumentsWorkspace from '../hooks/documents/useDocumentsWorkspace'; import { useDocumentsPreferences } from './useDocumentsPreferences'; import SettingsRoute from './SettingsRoute'; +import { WorkspaceSelectionProvider } from './WorkspaceSelectionContext'; const AppLayout: React.FC = () => { const documentsPreferences = useDocumentsPreferences(); @@ -16,6 +17,7 @@ const AppLayout: React.FC = () => { dropOverlayState, managementModals, contextValue, + workspaceSelection, settingsOpen, closeSettings, } = useDocumentsWorkspace({ @@ -50,8 +52,9 @@ const AppLayout: React.FC = () => { } return ( - -
+ + +
{ {settingsOpen ? ( ) : null} -
-
+
+
+ ); }; diff --git a/frontend/src/app/WorkspaceSelectionContext.tsx b/frontend/src/app/WorkspaceSelectionContext.tsx new file mode 100644 index 0000000..a5aa4eb --- /dev/null +++ b/frontend/src/app/WorkspaceSelectionContext.tsx @@ -0,0 +1,23 @@ +import React, { createContext, useContext } from 'react'; +import type useWorkspaceSelection from './useWorkspaceSelection'; + +type WorkspaceSelectionValue = ReturnType | null; + +const WorkspaceSelectionContext = createContext(null); + +export const WorkspaceSelectionProvider: React.FC<{ value: NonNullable; children: React.ReactNode }> + = ({ value, children }) => ( + + {children} + + ); + +export const useWorkspaceSelectionContext = () => { + const context = useContext(WorkspaceSelectionContext); + if (!context) { + throw new Error('useWorkspaceSelectionContext must be used within a WorkspaceSelectionProvider'); + } + return context; +}; + +export default WorkspaceSelectionContext; diff --git a/frontend/src/desktop/createDesktopSurface.tsx b/frontend/src/desktop/createDesktopSurface.tsx index b1ed1b0..204a6d4 100644 --- a/frontend/src/desktop/createDesktopSurface.tsx +++ b/frontend/src/desktop/createDesktopSurface.tsx @@ -4,6 +4,27 @@ import { createDocumentsTableHeaderActions } from '../documents/DocumentsPanel'; import createWorkspaceSurfaceConfig from '../documents/workspaceHeader'; import DocumentViewerPanel from '../preview/DocumentViewerPanel'; import DesktopWorkspace from './DesktopWorkspace'; +import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; + +type SelectionFloatingActionsBaseProps = Omit, 'selectionCount' | 'selectedDocumentIds' | 'selectedFolderIds'>; + +const SelectionFloatingActionsWithSelection: React.FC = (props) => { + const { selectedDocumentIds, selectedFolderIds } = useWorkspaceSelectionContext(); + const documentIds = Array.isArray(selectedDocumentIds) ? selectedDocumentIds : []; + const folderIds = Array.isArray(selectedFolderIds) ? selectedFolderIds : []; + const selectionCount = documentIds.length + folderIds.length; + if (selectionCount === 0) { + return null; + } + return ( + + ); +}; type WorkspaceProps = Record; @@ -36,8 +57,6 @@ const createDesktopSurface = ({ onRefresh, viewMode, onViewModeChange, - selectedDocumentIds, - selectedFolderIds, onDeleteSelection, onClearSelection, tags, @@ -60,9 +79,6 @@ const createDesktopSurface = ({ ? `${searchResults.length} matching document${searchResults.length === 1 ? '' : 's'}` : null; - const documentSelectionCount = Array.isArray(selectedDocumentIds) ? selectedDocumentIds.length : 0; - const folderSelectionCount = Array.isArray(selectedFolderIds) ? selectedFolderIds.length : 0; - const selectionCount = documentSelectionCount + folderSelectionCount; const actions = createDocumentsTableHeaderActions({ viewMode: viewMode || 'desk', onViewModeChange, @@ -71,28 +87,23 @@ const createDesktopSurface = ({ onToggleIncludeDescendants: onToggleSearchIncludeDescendants, }); - const floatingActions = selectionCount > 0 - ? ( - - ) - : null; + const floatingActions = ( + + ); const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null; const detail = detailOpen && detailProps diff --git a/frontend/src/documents/DocumentsGrid.tsx b/frontend/src/documents/DocumentsGrid.tsx index 3f06fc3..0a9e2cb 100644 --- a/frontend/src/documents/DocumentsGrid.tsx +++ b/frontend/src/documents/DocumentsGrid.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import type { DragEvent, MouseEvent, RefObject } from 'react'; import { FolderIcon, CheckIcon, CloseIcon } from '../ui/icons'; import DocumentThumbnailImage from './DocumentThumbnailImage'; @@ -7,6 +7,7 @@ import { getTagColorStyle } from '../utils/colors'; import { resolveCorrespondents } from './correspondents'; import { writeTagTransferData } from './tagTransfer'; import useInlineRename from './useInlineRename'; +import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; export type Identifier = string | number; @@ -55,8 +56,6 @@ type DocumentEventHandler = (document: DocumentLike, event: MouseEvent | null; - selectedFolderIdsSet?: Set | null; draggingDocumentIdsSet?: Set | null; draggedFolderId?: Identifier | 'root' | null; onFolderClick?: FolderEventHandler; @@ -82,14 +81,11 @@ interface DocumentsGridProps { scrollRef?: RefObject; onCorrespondentClick?: (correspondentId?: Identifier | null) => void; activeCorrespondentIdSet?: Set | null; - onClearSelection?: () => void; onDocumentRename?: (docId: Identifier, title: string) => Promise | boolean; } const DocumentsGrid: React.FC = ({ entries, - selectedDocumentIdsSet, - selectedFolderIdsSet, draggingDocumentIdsSet, draggedFolderId, onFolderClick, @@ -114,10 +110,16 @@ const DocumentsGrid: React.FC = ({ scrollRef, onCorrespondentClick, activeCorrespondentIdSet, - onClearSelection, onDocumentRename, onFolderRename, }) => { + const { + selectedDocumentIds, + selectedFolderIds, + clearSelection, + } = useWorkspaceSelectionContext(); + const selectedDocumentIdsSet = useMemo(() => new Set(selectedDocumentIds), [selectedDocumentIds]); + const selectedFolderIdsSet = useMemo(() => new Set(selectedFolderIds || []), [selectedFolderIds]); const { editingId: editingDocumentId, draftValue: documentDraft, @@ -157,7 +159,7 @@ const DocumentsGrid: React.FC = ({ style={{ '--documents-grid-icon-size': `${gridIconSize}px` }} onClick={(event) => { if (event.target === event.currentTarget) { - onClearSelection?.(); + clearSelection(); } }} > diff --git a/frontend/src/documents/DocumentsList.tsx b/frontend/src/documents/DocumentsList.tsx index d725e1e..e1c492d 100644 --- a/frontend/src/documents/DocumentsList.tsx +++ b/frontend/src/documents/DocumentsList.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import type { DragEvent, MouseEvent, RefObject } from 'react'; import { FolderIcon, CheckIcon, CloseIcon } from '../ui/icons'; import { getTagColorStyle } from '../utils/colors'; @@ -8,6 +8,7 @@ import CorrespondentLinks from './CorrespondentLinks'; import { resolveCorrespondents } from './correspondents'; import { writeTagTransferData } from './tagTransfer'; import useInlineRename from './useInlineRename'; +import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; export type Identifier = string | number; @@ -60,8 +61,6 @@ export type DocumentEventHandler = (document: DocumentLike, event: MouseEvent | null; - selectedFolderIdsSet?: Set | null; draggingDocumentIdsSet?: Set | null; draggedFolderId?: Identifier | 'root' | null; ensureAssetUrl?: (...args: any[]) => unknown; @@ -87,15 +86,12 @@ export interface DocumentsListProps { onCorrespondentClick?: (correspondentId?: Identifier | null) => void; activeCorrespondentIdSet?: Set | null; scrollRef?: RefObject; - onClearSelection?: () => void; } const DocumentsList: React.FC = ({ entries, focusedRowKey, - selectedDocumentIdsSet, - selectedFolderIdsSet, draggingDocumentIdsSet, draggedFolderId, ensureAssetUrl, @@ -121,8 +117,17 @@ const DocumentsList: React.FC = ({ onCorrespondentClick, activeCorrespondentIdSet, scrollRef, - onClearSelection, }) => { + const { + selectedDocumentIds, + selectedFolderIds, + clearSelection, + } = useWorkspaceSelectionContext(); + const selectedDocumentIdsSet = useMemo(() => new Set(selectedDocumentIds), [selectedDocumentIds]); + const selectedFolderIdsSet = useMemo( + () => new Set(selectedFolderIds || []), + [selectedFolderIds], + ); const { editingId: editingDocumentId, draftValue: documentDraft, @@ -160,7 +165,7 @@ const DocumentsList: React.FC = ({ { - onClearSelection?.(); + clearSelection(); }} > diff --git a/frontend/src/documents/hooks/useDocumentsPanelProps.ts b/frontend/src/documents/hooks/useDocumentsPanelProps.ts index 188afab..58f7a48 100644 --- a/frontend/src/documents/hooks/useDocumentsPanelProps.ts +++ b/frontend/src/documents/hooks/useDocumentsPanelProps.ts @@ -31,8 +31,6 @@ export interface UseDocumentsPanelPropsArgs { handleFolderRename?: (...args: unknown[]) => void; openDocumentPreview?: (...args: unknown[]) => void; handleDocumentTitleUpdate?: (...args: unknown[]) => void; - selectedDocumentIds?: Identifier[]; - selectedFolderIds?: Identifier[]; focusedRowKey?: Identifier | string | null; draggedDocumentIds?: Identifier[]; handleDocumentDragStart?: (...args: unknown[]) => void; @@ -40,8 +38,6 @@ export interface UseDocumentsPanelPropsArgs { searchLoading?: boolean; tagLookupById?: unknown; activeCorrespondentFilters?: Identifier[]; - selectedEntries?: unknown[]; - setFocusedRowKey?: (key: Identifier | string | null) => void; ensureAssetUrl?: (...args: unknown[]) => void; getDocumentAsset?: (...args: unknown[]) => unknown; toggleTagFilter?: (...args: unknown[]) => void; @@ -59,7 +55,6 @@ export interface UseDocumentsPanelPropsArgs { handleDeleteSelection?: () => void; handleEntryPointerCore?: (...args: unknown[]) => void; inspectDocument?: (docId: Identifier | null, metadata?: unknown) => void; - handleEntrySelection?: (...args: unknown[]) => void; tags?: unknown[]; correspondents?: unknown[]; documentLookup?: unknown; @@ -91,8 +86,6 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => { handleFolderRename, openDocumentPreview, handleDocumentTitleUpdate, - selectedDocumentIds, - selectedFolderIds, focusedRowKey, draggedDocumentIds, handleDocumentDragStart, @@ -100,8 +93,6 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => { searchLoading, tagLookupById, activeCorrespondentFilters, - selectedEntries, - setFocusedRowKey, ensureAssetUrl, getDocumentAsset, toggleTagFilter, @@ -115,11 +106,9 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => { searchIncludeDescendants, toggleSearchIncludeDescendants, handleDocumentsViewModeChange, - clearDocumentSelection, handleDeleteSelection, handleEntryPointerCore, inspectDocument, - handleEntrySelection, tags, correspondents, documentLookup, @@ -151,8 +140,6 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => { onFolderRename: handleFolderRename, onDocumentOpen: openDocumentPreview, onDocumentRename: handleDocumentTitleUpdate, - selectedDocumentIds, - selectedFolderIds, focusedRowKey, draggingDocumentIds: draggedDocumentIds, onDocumentDragStart: handleDocumentDragStart, @@ -160,8 +147,6 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => { isSearchLoading: searchLoading, tagLookupById, activeCorrespondentIds: activeCorrespondentFilters, - selectedEntries, - onFocusedRowChange: setFocusedRowKey, ensureAssetUrl, getDocumentAsset, onTagClick: toggleTagFilter, @@ -175,11 +160,9 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => { searchIncludeDescendants, onToggleSearchIncludeDescendants: toggleSearchIncludeDescendants, onViewModeChange: handleDocumentsViewModeChange, - onClearSelection: clearDocumentSelection, onDeleteSelection: handleDeleteSelection, onEntryPointer: handleEntryPointerCore, onInspectDocument: inspectDocument, - onEntrySelection: handleEntrySelection, tags, correspondents, documentLookup, @@ -194,7 +177,6 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => { [ activeCorrespondentFilters, breadcrumbs, - clearDocumentSelection, correspondents, currentFolderName, currentSubfolders, @@ -221,7 +203,6 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => { handleDocumentsSortFieldChange, handleDocumentsViewModeChange, handleEntryPointerCore, - handleEntrySelection, handleFolderDragEnd, handleFolderDragStart, handleFolderRename, @@ -233,11 +214,7 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => { searchIncludeDescendants, searchLoading, searchResults, - selectedDocumentIds, - selectedEntries, - selectedFolderIds, selectFolder, - setFocusedRowKey, tagLookupById, tags, toggleCorrespondentFilter, diff --git a/frontend/src/documents/panel/DocumentsPanel.tsx b/frontend/src/documents/panel/DocumentsPanel.tsx index 1b7b307..b30aecc 100644 --- a/frontend/src/documents/panel/DocumentsPanel.tsx +++ b/frontend/src/documents/panel/DocumentsPanel.tsx @@ -7,6 +7,7 @@ import { isTagTransferEvent } from '../tagTransfer'; import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay'; import { useAssetNavigator } from '../../hooks/useAssetNavigator'; import { isPointerModifierEvent, isPrimaryPointerEvent } from '../useEntryPointer'; +import { useWorkspaceSelectionContext } from '../../app/WorkspaceSelectionContext'; const DEFAULT_GRID_ICON_SIZE = 144; @@ -37,19 +38,14 @@ const DocumentsPanel: React.FC = ({ onFolderDragEnd, draggedFolderId, onFolderRename, - selectedFolderIds = [], - selectedDocumentIds = [], - focusedRowKey, draggingDocumentIds = [], onDocumentDragStart, onDocumentDragEnd, onDocumentRename, onEntryPointer = null, - onEntrySelection = null, onInspectDocument = null, tagLookupById, activeCorrespondentIds = [], - onFocusedRowChange, ensureAssetUrl = null, getDocumentAsset = defaultGetDocumentAsset, onTagClick, @@ -58,10 +54,15 @@ const DocumentsPanel: React.FC = ({ onDocumentTagDrop, viewMode = 'list', onViewModeChange, - onClearSelection, - selectedEntries = [], showHeader = true, }) => { + const { + selectedEntries, + focusedRowKey, + setFocusedRowKey, + handleEntrySelection, + clearSelection, + } = useWorkspaceSelectionContext(); const showingSearchResults = searchResults !== null; const rows = showingSearchResults ? searchResults : documents; @@ -89,9 +90,9 @@ const DocumentsPanel: React.FC = ({ const changed = previous.type !== nextContext.type || previous.marker !== nextContext.marker; if (changed) { - onClearSelection?.(); + clearSelection(); } - }, [showingSearchResults, currentFolderId, searchResults, onClearSelection]); + }, [showingSearchResults, currentFolderId, searchResults, clearSelection]); const entries = useMemo(() => { const list = []; @@ -112,14 +113,6 @@ const DocumentsPanel: React.FC = ({ return list; }, [showingSearchResults, subfolders, rows]); - const selectedSet = useMemo( - () => new Set(selectedDocumentIds), - [selectedDocumentIds], - ); - const selectedFolderSet = useMemo( - () => new Set(selectedFolderIds || []), - [selectedFolderIds], - ); const draggingSet = useMemo( () => new Set(draggingDocumentIds || []), [draggingDocumentIds], @@ -271,12 +264,12 @@ const DocumentsPanel: React.FC = ({ return; } - onFocusedRowChange?.(resolvedKey); + setFocusedRowKey(resolvedKey); }, [ focusedRowKey, navigableRowKeys, navigableRows, - onFocusedRowChange, + setFocusedRowKey, selectedEntries, ]); @@ -320,7 +313,7 @@ const DocumentsPanel: React.FC = ({ if (key === 'Enter' || key === ' ' || key === 'Space' || key === 'Spacebar') { if (activeRow) { - onEntrySelection?.(activeRow.key, event); + handleEntrySelection(activeRow.key, event); if (activeRow.type === EntryType.folder) { onFolderSelect?.(activeRow.id); } else { @@ -353,8 +346,8 @@ const DocumentsPanel: React.FC = ({ return; } - onFocusedRowChange?.(targetRow.key); - onEntrySelection?.(targetRow.key, { + setFocusedRowKey(targetRow.key); + handleEntrySelection(targetRow.key, { shiftKey, preventDefault: () => {}, }); @@ -364,11 +357,11 @@ const DocumentsPanel: React.FC = ({ getEntryByKey, navigableRowKeys, navigableRows, - onEntrySelection, - onFocusedRowChange, onFolderSelect, selectedEntries, handleDocumentPreviewZoom, + handleEntrySelection, + setFocusedRowKey, ], ); @@ -529,10 +522,10 @@ const DocumentsPanel: React.FC = ({ && scrollRef.current ) { scrollRef.current.focus({ preventScroll: true }); - onFocusedRowChange?.(`folder:${folder.id}`); + setFocusedRowKey(`folder:${folder.id}`); } }, - [onEntryPointer, onFocusedRowChange], + [onEntryPointer, setFocusedRowKey], ); const handleDocumentDragStartLocal = useCallback( @@ -666,7 +659,7 @@ const DocumentsPanel: React.FC = ({ }} onClick={(event) => { if (event.target === event.currentTarget) { - onClearSelection?.(); + clearSelection(); } }} aria-activedescendant={isGridView ? undefined : activeDescendantId} @@ -674,8 +667,6 @@ const DocumentsPanel: React.FC = ({ {isGridView ? ( = ({ scrollRef={scrollRef} onCorrespondentClick={onCorrespondentClick} activeCorrespondentIdSet={activeCorrespondentIdSet} - onClearSelection={onClearSelection} onDocumentRename={onDocumentRename} onFolderRename={onFolderRename} /> @@ -708,8 +698,6 @@ const DocumentsPanel: React.FC = ({ = ({ onCorrespondentClick={onCorrespondentClick} activeCorrespondentIdSet={activeCorrespondentIdSet} scrollRef={scrollRef} - onClearSelection={onClearSelection} /> )} diff --git a/frontend/src/documents/panel/createDocumentsSurface.tsx b/frontend/src/documents/panel/createDocumentsSurface.tsx index ecb9ce5..171c831 100644 --- a/frontend/src/documents/panel/createDocumentsSurface.tsx +++ b/frontend/src/documents/panel/createDocumentsSurface.tsx @@ -4,6 +4,27 @@ import SelectionFloatingActions from '../SelectionFloatingActions'; import createWorkspaceSurfaceConfig from '../workspaceHeader'; import DocumentsPanel from './DocumentsPanel'; import { createDocumentsTableHeaderActions } from './DocumentsToolbar'; +import { useWorkspaceSelectionContext } from '../../app/WorkspaceSelectionContext'; + +type SelectionFloatingActionsBaseProps = Omit, 'selectionCount' | 'selectedDocumentIds' | 'selectedFolderIds'>; + +const SelectionFloatingActionsWithSelection: React.FC = (props) => { + const { selectedDocumentIds, selectedFolderIds } = useWorkspaceSelectionContext(); + const documentIds = Array.isArray(selectedDocumentIds) ? selectedDocumentIds : []; + const folderIds = Array.isArray(selectedFolderIds) ? selectedFolderIds : []; + const selectionCount = documentIds.length + folderIds.length; + if (selectionCount === 0) { + return null; + } + return ( + + ); +}; interface CreateDocumentsSurfaceArgs { tableProps: Record; @@ -30,8 +51,6 @@ const createDocumentsSurface = ({ sortDirection, onSortFieldChange, onSortDirectionToggle, - selectedDocumentIds, - selectedFolderIds, onDeleteSelection, onClearSelection, tags, @@ -55,10 +74,6 @@ const createDocumentsSurface = ({ ? `${searchResults.length} matching document${searchResults.length === 1 ? '' : 's'}` : null; - const documentSelectionCount = Array.isArray(selectedDocumentIds) ? selectedDocumentIds.length : 0; - const folderSelectionCount = Array.isArray(selectedFolderIds) ? selectedFolderIds.length : 0; - const selectionCount = documentSelectionCount + folderSelectionCount; - const actions = createDocumentsTableHeaderActions({ viewMode, onViewModeChange, @@ -72,28 +87,23 @@ const createDocumentsSurface = ({ onToggleIncludeDescendants: onToggleSearchIncludeDescendants, }); - const floatingActions = selectionCount > 0 - ? ( - - ) - : null; + const floatingActions = ( + + ); const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null; const detail = detailOpen && detailProps diff --git a/frontend/src/hooks/documents/useDocumentsWorkspace.ts b/frontend/src/hooks/documents/useDocumentsWorkspace.ts index fae0d14..aedd35a 100644 --- a/frontend/src/hooks/documents/useDocumentsWorkspace.ts +++ b/frontend/src/hooks/documents/useDocumentsWorkspace.ts @@ -1655,6 +1655,7 @@ const useDocumentsWorkspace = ({ dropOverlayState, managementModals, contextValue, + workspaceSelection: selection, settingsOpen, closeSettings, }; diff --git a/frontend/src/styles/documents/listing.css b/frontend/src/styles/documents/listing.css index 415b76b..2bfdc32 100644 --- a/frontend/src/styles/documents/listing.css +++ b/frontend/src/styles/documents/listing.css @@ -67,7 +67,7 @@ display: flex; justify-content: center; pointer-events: none; - z-index: 6; + z-index: 20000; height: 0; width: calc(100% - 2rem); margin: 0 1rem;