diff --git a/frontend/src/app/useDocumentSelection.ts b/frontend/src/app/useDocumentSelection.ts index 6005fc8..446364c 100644 --- a/frontend/src/app/useDocumentSelection.ts +++ b/frontend/src/app/useDocumentSelection.ts @@ -35,23 +35,23 @@ export const useDocumentSelection = ({ const selectionAnchorRef = useRef(null); const selectionInitializedRef = useRef(false); const [focusedDocumentId, setFocusedDocumentId] = useState(null); - const [focusedRowKey, setFocusedRowKey] = useState(null); + const [focusedEntryKey, setFocusedEntryKey] = useState(null); - const visibleRowKeySetRef = useRef>(new Set()); - const navigableRowKeysRef = useRef([]); + const visibleEntryKeySetRef = useRef>(new Set()); + const navigableEntryKeysRef = useRef([]); const configureSelectionEnvironment = useCallback(({ - visibleRowKeySet, - navigableRowKeys, + visibleEntryKeySet, + navigableEntryKeys, }: { - visibleRowKeySet?: Set; - navigableRowKeys?: string[]; + visibleEntryKeySet?: Set; + navigableEntryKeys?: string[]; }) => { - if (visibleRowKeySet) { - visibleRowKeySetRef.current = visibleRowKeySet; + if (visibleEntryKeySet) { + visibleEntryKeySetRef.current = visibleEntryKeySet; } - if (Array.isArray(navigableRowKeys)) { - navigableRowKeysRef.current = navigableRowKeys; + if (Array.isArray(navigableEntryKeys)) { + navigableEntryKeysRef.current = navigableEntryKeys; } }, []); @@ -88,16 +88,16 @@ export const useDocumentSelection = ({ const applySelection = useCallback( ( - rowKeys: Array, + entryKeys: Array, { anchor = null, interactedKeys = [] }: ApplySelectionOptions = { anchor: null, interactedKeys: [] }, ) => { - const visibleRowKeySet = visibleRowKeySetRef.current; + const visibleEntryKeySet = visibleEntryKeySetRef.current; const unique: string[] = []; - (rowKeys || []).forEach((key) => { + (entryKeys || []).forEach((key) => { if (!key) return; let canonicalKey: string | null = null; - if (visibleRowKeySet.has(key)) { + if (visibleEntryKeySet.has(key)) { canonicalKey = key; } else if (isDocumentEntry(key)) { const id = getEntryId(key); @@ -107,7 +107,7 @@ export const useDocumentSelection = ({ canonicalKey = id ? createFolderEntryKey(id) : null; } - if (!canonicalKey || !visibleRowKeySet.has(canonicalKey)) { + if (!canonicalKey || !visibleEntryKeySet.has(canonicalKey)) { return; } @@ -159,19 +159,25 @@ export const useDocumentSelection = ({ ); const clearSelection = useCallback(() => { - setFocusedRowKey(null); + setFocusedEntryKey(null); applySelection([], { anchor: null, interactedKeys: [] }); }, [applySelection]); const handleEntrySelection = useCallback( - (rowKey: string, event?: SelectionEventLike) => { - const visibleRowKeySet = visibleRowKeySetRef.current; - const navigableRowKeys = navigableRowKeysRef.current; - if (!rowKey || !visibleRowKeySet.has(rowKey)) { + (entryKeyOrKeys: string | string[], event?: SelectionEventLike) => { + const visibleEntryKeySet = visibleEntryKeySetRef.current; + const navigableEntryKeys = navigableEntryKeysRef.current; + + const entryKeys = Array.isArray(entryKeyOrKeys) ? entryKeyOrKeys : [entryKeyOrKeys]; + const validKeys = entryKeys.filter((key) => key && visibleEntryKeySet.has(key)); + + if (validKeys.length === 0) { return; } - setFocusedRowKey(rowKey); + // Focus the last valid key + const lastKey = validKeys[validKeys.length - 1]; + setFocusedEntryKey(lastKey); const shiftKey = Boolean(event?.shiftKey); const metaKey = Boolean(event?.metaKey); @@ -187,44 +193,57 @@ export const useDocumentSelection = ({ anchorKey = selectedEntries[selectedEntries.length - 1]; } if (!anchorKey) { - anchorKey = rowKey; + anchorKey = lastKey; } let nextKeys: string[] = []; let interactedKeys: string[] = []; - if (shiftKey && anchorKey) { - const anchorIndex = navigableRowKeys.indexOf(anchorKey); - const targetIndex = navigableRowKeys.indexOf(rowKey); + // Shift selection logic (range) - primarily for single click + shift + if (shiftKey && anchorKey && validKeys.length === 1) { + const entryKey = validKeys[0]; + const anchorIndex = navigableEntryKeys.indexOf(anchorKey); + const targetIndex = navigableEntryKeys.indexOf(entryKey); if (anchorIndex !== -1 && targetIndex !== -1) { const [start, end] = anchorIndex <= targetIndex ? [anchorIndex, targetIndex] : [targetIndex, anchorIndex]; - const range = navigableRowKeys.slice(start, end + 1); + const range = navigableEntryKeys.slice(start, end + 1); nextKeys = range; const previousSet = new Set(selectedEntries); - interactedKeys = range.filter((key) => key === rowKey || !previousSet.has(key)); - if (!interactedKeys.includes(rowKey)) { - interactedKeys.push(rowKey); + interactedKeys = range.filter((key) => key === entryKey || !previousSet.has(key)); + if (!interactedKeys.includes(entryKey)) { + interactedKeys.push(entryKey); } } else { - nextKeys = [rowKey]; - interactedKeys = [rowKey]; + nextKeys = [entryKey]; + interactedKeys = [entryKey]; } } else if (additive) { - if (selectedEntries.includes(rowKey)) { - nextKeys = selectedEntries.filter((key) => key !== rowKey); - interactedKeys = []; + // Additive batch + const previousSet = new Set(selectedEntries); + if (validKeys.length === 1) { + const entryKey = validKeys[0]; + if (previousSet.has(entryKey)) { + nextKeys = selectedEntries.filter((key) => key !== entryKey); + interactedKeys = []; + } else { + nextKeys = [...selectedEntries, entryKey]; + interactedKeys = [entryKey]; + } } else { - nextKeys = [...selectedEntries, rowKey]; - interactedKeys = [rowKey]; + // Batch add + validKeys.forEach(key => previousSet.add(key)); + nextKeys = Array.from(previousSet) as string[]; + interactedKeys = validKeys; } - anchorKey = rowKey; + anchorKey = lastKey; } else { - nextKeys = [rowKey]; - interactedKeys = [rowKey]; - anchorKey = rowKey; + // Replace with batch + nextKeys = validKeys; + interactedKeys = validKeys; + anchorKey = lastKey; } applySelection(nextKeys, { anchor: anchorKey, interactedKeys }); @@ -235,14 +254,14 @@ export const useDocumentSelection = ({ const promoteSelectionOrder = useCallback( (docId?: DocumentId | null) => { if (!docId) return; - const rowKey = createDocumentEntryKey(docId); - if (!rowKey) return; + const entryKey = createDocumentEntryKey(docId); + if (!entryKey) return; - if (!selectedEntries.includes(rowKey)) { + if (!selectedEntries.includes(entryKey)) { return; } - updateSelectionOrder(selectedEntries, [rowKey]); + updateSelectionOrder(selectedEntries, [entryKey]); }, [selectedEntries, updateSelectionOrder], ); @@ -257,8 +276,8 @@ export const useDocumentSelection = ({ selectionInitializedRef, focusedDocumentId, setFocusedDocumentId, - focusedRowKey, - setFocusedRowKey, + focusedEntryKey, + setFocusedEntryKey, applySelection, clearSelection, handleEntrySelection, diff --git a/frontend/src/app/useWorkspaceSelection.ts b/frontend/src/app/useWorkspaceSelection.ts index f6811f3..11fdf02 100644 --- a/frontend/src/app/useWorkspaceSelection.ts +++ b/frontend/src/app/useWorkspaceSelection.ts @@ -5,7 +5,7 @@ import { isDocumentEntry, isFolderEntry, getEntryId } from './entryKey'; interface SelectionEntry { entryKey?: string; // Legacy field for compatibility - rowKey?: string; + // rowKey?: string; // Removed as part of refactor [key: string]: unknown; } @@ -32,8 +32,8 @@ export const useWorkspaceSelection = ({ selectionInitializedRef, focusedDocumentId, setFocusedDocumentId, - focusedRowKey, - setFocusedRowKey, + focusedEntryKey, + setFocusedEntryKey, applySelection, clearSelection, handleEntrySelection, @@ -60,12 +60,18 @@ export const useWorkspaceSelection = ({ ); const selectEntry = useCallback( - (entry: SelectionEntry | string, event?: unknown) => { - const rowKey = entry && Object(entry) === entry - ? (entry as SelectionEntry).rowKey ?? undefined - : (entry as string); - if (!rowKey) return; - handleEntrySelection(rowKey, event); + (entryOrEntries: SelectionEntry | string | Array, event?: unknown) => { + const entries = Array.isArray(entryOrEntries) ? entryOrEntries : [entryOrEntries]; + const entryKeys = entries + .map((entry) => { + return entry && Object(entry) === entry + ? (entry as SelectionEntry).entryKey ?? undefined + : (entry as string); + }) + .filter((key): key is string => Boolean(key)); + + if (entryKeys.length === 0) return; + handleEntrySelection(entryKeys, event); }, [handleEntrySelection], ); @@ -96,8 +102,8 @@ export const useWorkspaceSelection = ({ selectionInitializedRef, focusedDocumentId, setFocusedDocumentId, - focusedRowKey, - setFocusedRowKey, + focusedEntryKey, + setFocusedEntryKey, applySelection, clearSelection, handleEntrySelection: selectEntry, diff --git a/frontend/src/desktop/DesktopWorkspace.tsx b/frontend/src/desktop/DesktopWorkspace.tsx index 51022f9..5c839a9 100644 --- a/frontend/src/desktop/DesktopWorkspace.tsx +++ b/frontend/src/desktop/DesktopWorkspace.tsx @@ -171,6 +171,8 @@ interface DesktopWorkspaceViewProps extends Omit void; dragSettings: DragSettings; markLayoutDirty: () => void; + onSelect?: (descriptor: unknown, event?: unknown) => void; + onPromoteSelection?: (docId: Identifier, event?: unknown) => void; } const defaultGetDocumentAsset: GetAsset = () => null; @@ -192,6 +194,7 @@ const DesktopWorkspace: React.FC = ({ clearSelection, handleEntrySelection, promoteSelectionOrder, + configureSelectionEnvironment, } = useWorkspaceSelectionContext(); const items = useMemo( () => { @@ -213,36 +216,16 @@ const DesktopWorkspace: React.FC = ({ [entries], ); - const getDocRowKey = useCallback((id: Identifier | null) => (id != null ? `document:${id}` : null), []); - - const handleStackSelect = useCallback( - (docIds: Identifier[], event?: PointerEvent | MouseEvent | null) => { - if (!Array.isArray(docIds) || docIds.length === 0) { - return; - } - const syntheticEvent = event || ({ - metaKey: true, - ctrlKey: true, - preventDefault: () => { }, - } as unknown as PointerEvent); - docIds.forEach((id) => { - const key = getDocRowKey(id); - if (key) { - handleEntrySelection(key, syntheticEvent); - } - }); - }, - [getDocRowKey, handleEntrySelection], - ); + const getDocEntryKey = useCallback((id: Identifier | null) => (id != null ? `document:${id}` : null), []); const handlePromoteSelection = useCallback( (docId: Identifier | null) => { - const key = getDocRowKey(docId); - if (key && promoteSelectionOrder) { - promoteSelectionOrder(key); + const key = getDocEntryKey(docId); + if (key) { + promoteSelectionOrder(docId); } }, - [getDocRowKey, promoteSelectionOrder], + [getDocEntryKey, promoteSelectionOrder], ); const allowLayoutPersistence = Boolean(tenantId && viewId && viewId.startsWith('folder:')); @@ -307,10 +290,6 @@ const DesktopWorkspace: React.FC = ({ engine.setDocumentLookup(map); }, [engine, items]); - useEffect(() => { - engine.setEnsureDocumentSize(ensureDocumentSize); - }, [engine, ensureDocumentSize]); - const workspaceSnapshot = useWorkspaceSnapshot(engine, useSyncExternalStore) as WorkspaceSnapshotState; const { layout: layoutSnapshot, @@ -323,6 +302,19 @@ const DesktopWorkspace: React.FC = ({ initialLoadDone, } = workspaceSnapshot; + useEffect(() => { + const visibleEntryKeySet = new Set(); + visibleDocIds.forEach((id) => { + const key = getDocEntryKey(id); + if (key) visibleEntryKeySet.add(key); + }); + + configureSelectionEnvironment({ + visibleEntryKeySet, + navigableEntryKeys: [], // Desktop doesn't have linear navigation yet + }); + }, [visibleDocIds, getDocEntryKey, configureSelectionEnvironment]); + useEffect(() => { const shouldWaitForPersisted = allowLayoutPersistence && !initialLoadDone; @@ -794,10 +786,22 @@ const DesktopWorkspace: React.FC = ({ overlayOriginRect, overlayOriginTransform, overlayDocument, - onDocumentStackSelect: handleStackSelect, onPromoteSelection: handlePromoteSelection, selectedDocumentIds, onClearSelection: clearSelection, + onDocumentClick: passThroughProps.onDocumentClick, + onSelect: (descriptorOrDescriptors: any, event: any) => { + const descriptors = Array.isArray(descriptorOrDescriptors) + ? descriptorOrDescriptors + : [descriptorOrDescriptors]; + const keys = descriptors + .map((d: any) => getDocEntryKey(d.id)) + .filter((k: any) => k); + + if (keys.length > 0) { + handleEntrySelection(keys, event); + } + }, documentLookup, resolveBaseMetrics, bringToFront, @@ -836,7 +840,6 @@ function DesktopWorkspaceView({ overlayOriginTransform, overlayDocument, onDocumentClick, - onDocumentStackSelect, onPromoteSelection, selectedDocumentIds, onClearSelection, @@ -850,6 +853,7 @@ function DesktopWorkspaceView({ dragSettings, onDocumentActivate, markLayoutDirty, + onSelect, }: DesktopWorkspaceViewProps) { const handleDeskDocumentActivate = useCallback( (docId: Identifier) => { @@ -888,7 +892,6 @@ function DesktopWorkspaceView({ settings: dragSettings, containerRef, onDocumentActivate: handleDeskDocumentActivate, - selectedDocumentIds, markLayoutDirty, }) as { handlePointerDown: React.PointerEventHandler; @@ -908,11 +911,11 @@ function DesktopWorkspaceView({ handlePointerUp, handlePointerCancel, onDocumentClick: handleDeskDocumentClick, - onDocumentStackSelect, onPromoteSelection, onDocumentActivate: handleDeskDocumentActivate, selectedDocumentIds, openOverlayForDoc, + onSelect, }); useEffect(() => { diff --git a/frontend/src/desktop/pointer/pointerUtils.ts b/frontend/src/desktop/pointer/pointerUtils.ts index 31a7a2c..36b81b2 100644 --- a/frontend/src/desktop/pointer/pointerUtils.ts +++ b/frontend/src/desktop/pointer/pointerUtils.ts @@ -42,6 +42,7 @@ export interface PointerIntent { clickSelectionApplied: boolean; stackSelectionApplied: boolean; longPressTriggered: boolean; + optimisticSelection: string[]; } export const createPointerIntent = ({ @@ -61,9 +62,9 @@ export const createPointerIntent = ({ if (metaKey) { clickAction = CLICK_ACTIONS.addStack; - dragAction = DRAG_ACTIONS.dragSelectStack; + dragAction = DRAG_ACTIONS.dragSelection; } else if (alreadySelected) { - clickAction = CLICK_ACTIONS.selectSingle; + clickAction = CLICK_ACTIONS.openDetail; dragAction = selectionCount > 1 ? DRAG_ACTIONS.dragSelection : DRAG_ACTIONS.dragSelectSingle; } else { clickAction = CLICK_ACTIONS.selectSingle; @@ -74,8 +75,23 @@ export const createPointerIntent = ({ ? stackHits.map((value) => String(value)) : [String(doc.id)]; - const stackDocIdsForDrag = dragAction === DRAG_ACTIONS.dragSelectStack ? stackList : null; const stackDocIdsForClick = clickAction === CLICK_ACTIONS.addStack ? stackList : null; + const stackDocIdsForDrag = metaKey ? stackList : null; + + // Calculate optimistic selection + let optimisticSelection: string[] = []; + if (metaKey) { + // Additive selection (stack or single) + const currentSelection = new Set(selectedDocumentIds); + stackList.forEach(id => currentSelection.add(id)); + optimisticSelection = Array.from(currentSelection); + } else if (alreadySelected) { + // Already selected: keep current selection + optimisticSelection = [...selectedDocumentIds]; + } else { + // New single selection + optimisticSelection = [doc.id]; + } return { docId: doc.id, @@ -90,33 +106,53 @@ export const createPointerIntent = ({ stackDocIdsForDrag, stackDocIdsForClick, stackReplaceOnClick: clickAction === CLICK_ACTIONS.addStack, - stackReplaceOnDrag: dragAction === DRAG_ACTIONS.dragSelectStack, + stackReplaceOnDrag: false, clickSelectionApplied: false, stackSelectionApplied: false, longPressTriggered: false, + optimisticSelection, }; }; -export const applyClickPlanImmediately = ({ intent, event, onEntryPointer, onDocumentStackSelect }: { +export const applyClickPlanImmediately = ({ intent, event, onEntryPointer, onDocumentStackSelect, onSelect }: { intent: PointerIntent; event?: unknown; onEntryPointer?: (descriptor: unknown, event?: unknown) => void; onDocumentStackSelect?: (docIds: string[], event?: unknown, options?: { replace?: boolean }) => void; + onSelect?: (descriptor: unknown, event?: unknown) => void; }) => { switch (intent.clickAction) { case CLICK_ACTIONS.selectSingle: case CLICK_ACTIONS.addCard: - safeInvoke(onEntryPointer, intent.entryDescriptor, event); + if (onSelect) { + safeInvoke(onSelect, intent.entryDescriptor, event); + } else { + safeInvoke(onEntryPointer, intent.entryDescriptor, event); + } intent.clickSelectionApplied = true; break; case CLICK_ACTIONS.addStack: if (Array.isArray(intent.stackDocIdsForClick) && intent.stackDocIdsForClick.length > 0) { - safeInvoke( - onDocumentStackSelect, - intent.stackDocIdsForClick, - event, - { replace: intent.stackReplaceOnClick }, - ); + // Use onSelect for stack selection (batch) + if (onSelect) { + // Map docIds to descriptors if necessary, or just pass IDs if onSelect handles it. + // The current onSelect adapter in DesktopWorkspace expects { id } objects or just IDs? + // Let's assume it expects descriptors like selectSingle. + const descriptors = intent.stackDocIdsForClick.map(id => ({ + type: 'document', + id, + key: `document:${id}`, + })); + safeInvoke(onSelect, descriptors, event); + } else { + // Fallback to legacy if onSelect not provided (shouldn't happen in new flow) + safeInvoke( + onDocumentStackSelect, + intent.stackDocIdsForClick, + event, + { replace: intent.stackReplaceOnClick }, + ); + } intent.clickSelectionApplied = true; intent.stackSelectionApplied = true; } @@ -128,17 +164,18 @@ export const applyClickPlanImmediately = ({ intent, event, onEntryPointer, onDoc } }; -export const finalizeClickSelection = ({ intent, event, onEntryPointer, onDocumentStackSelect }: { +export const finalizeClickSelection = ({ intent, event, onEntryPointer, onDocumentStackSelect, onSelect }: { intent: PointerIntent; event?: unknown; onEntryPointer?: (descriptor: unknown, event?: unknown) => void; onDocumentStackSelect?: (docIds: string[], event?: unknown, options?: { replace?: boolean }) => void; + onSelect?: (descriptor: unknown, event?: unknown) => void; }) => { if (!intent || intent.clickSelectionApplied) { return; } - applyClickPlanImmediately({ intent, event, onEntryPointer, onDocumentStackSelect }); + applyClickPlanImmediately({ intent, event, onEntryPointer, onDocumentStackSelect, onSelect }); }; export const applyLongPressSelection = ({ intent, stackDocIds, syntheticEvent, onDocumentStackSelect }: { diff --git a/frontend/src/desktop/pointer/useDeskPointer.js b/frontend/src/desktop/pointer/useDeskPointer.js index 383acaa..c2d79d8 100644 --- a/frontend/src/desktop/pointer/useDeskPointer.js +++ b/frontend/src/desktop/pointer/useDeskPointer.js @@ -33,11 +33,11 @@ export const useDeskPointer = ({ handlePointerUp, handlePointerCancel, onDocumentClick, - onDocumentStackSelect, onPromoteSelection, onDocumentActivate, selectedDocumentIds, openOverlayForDoc = null, + onSelect = null, }) => { const pointerIntentRef = useRef(null); const pointerStartRef = useRef({ x: 0, y: 0 }); @@ -203,13 +203,13 @@ export const useDeskPointer = ({ intent, stackDocIds: stackHits, syntheticEvent, - onDocumentStackSelect, + onDocumentStackSelect: null, // Deprecated, handled by onSelect if needed, or long press needs update }); pointerIntentRef.current = intent; resetLongPressState(); }, LONG_PRESS_DURATION_MS); }, - [onDocumentStackSelect, resolveStackDocIds, resetLongPressState], + [resolveStackDocIds, resetLongPressState], ); useEffect(() => () => resetLongPressState(), [resetLongPressState]); @@ -242,25 +242,24 @@ export const useDeskPointer = ({ stackHits, }); - if (intent.selectedAtDown) { - safeInvoke(onPromoteSelection, doc.id, event); - } + if (intent.selectedAtDown) { + safeInvoke(onPromoteSelection, doc.id, event); + } - applyClickPlanImmediately({ - intent, - event, - onEntryPointer: onDocumentClick, - onDocumentStackSelect, - }); + applyClickPlanImmediately({ + intent, + event, + onEntryPointer: onDocumentClick, + onSelect, + }); pointerIntentRef.current = intent; handlePointerDown(event, doc.id, { - stackDocIds: intent.stackDocIdsForDrag, + draggedDocIds: intent.optimisticSelection, stackSelectionApplied: intent.stackSelectionApplied, wasSelected: intent.selectedAtDown, modifierActive, - stackReplace: intent.stackReplaceOnDrag, }); scheduleLongPress({ @@ -273,7 +272,7 @@ export const useDeskPointer = ({ handlePointerDown, onPromoteSelection, onDocumentClick, - onDocumentStackSelect, + onSelect, resolveStackDocIds, resetLongPressState, scheduleLongPress, @@ -309,7 +308,7 @@ export const useDeskPointer = ({ intent: pointerState, event, onEntryPointer: onDocumentClick, - onDocumentStackSelect, + onSelect, }); if ( @@ -336,8 +335,8 @@ export const useDeskPointer = ({ [ handlePointerUp, onDocumentActivate, - onDocumentStackSelect, onDocumentClick, + onSelect, resetLongPressState, selectedDocumentIds, ], diff --git a/frontend/src/desktop/useDocumentDrag.ts b/frontend/src/desktop/useDocumentDrag.ts index 1134741..f1a9873 100644 --- a/frontend/src/desktop/useDocumentDrag.ts +++ b/frontend/src/desktop/useDocumentDrag.ts @@ -6,7 +6,7 @@ import { type RefObject, } from 'react'; import type { PointerEvent as ReactPointerEvent } from 'react'; -import { preventAll, safeInvoke } from './events'; +import { preventAll } from './events'; import { clamp } from '../utils/math'; import usePointerTap from '../ui/usePointerTap'; import { @@ -83,11 +83,9 @@ interface DragSettings { } interface PointerDownOptions { - stackDocIds?: Array; + draggedDocIds?: string[]; stackSelectionApplied?: boolean; - wasSelected?: boolean; modifierActive?: boolean; - stackReplace?: boolean; } interface UseDocumentDragOptions { @@ -109,12 +107,6 @@ interface UseDocumentDragOptions { settings?: DragSettings; containerRef?: RefObject; onDocumentActivate?: (docId: Identifier | null, event?: PointerEvent | ReactPointerEvent) => void; - onDocumentStackSelect?: ( - docIds: Identifier[], - event: PointerEvent | ReactPointerEvent, - options?: { replace?: boolean }, - ) => void; - selectedDocumentIds?: Array; markLayoutDirty?: () => void; } @@ -152,9 +144,7 @@ interface DragStateInternal extends EngineDragState { activeDocIds: string[]; groupItems: DragGroupItemInternal[]; groupElevated: boolean; - stackDocIds: string[] | null; stackSelectionApplied: boolean; - stackReplace: boolean; massGrams: number; pointerRadiusScale: number; lastPointerCanvasX: number; @@ -209,8 +199,6 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { settings, containerRef: providedContainerRef, onDocumentActivate, - onDocumentStackSelect, - selectedDocumentIds = [], markLayoutDirty, } = options; @@ -342,50 +330,20 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { return; } const massGrams = computeDocumentMassGrams(doc); + const draggedDocIds = options?.draggedDocIds; - const stackDocIdsOptionRaw = options?.stackDocIds; - const stackDocIdsOption = Array.isArray(stackDocIdsOptionRaw) - ? stackDocIdsOptionRaw - .map((value) => (value != null ? String(value) : null)) - .filter((value): value is string => Boolean(value)) - : null; - const stackSelectionAppliedInitial = Boolean(options?.stackSelectionApplied); - const wasSelectedAtPointerDown = Boolean(options?.wasSelected); - const pointerModifierActive = - options?.modifierActive ?? Boolean(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey); - const stackReplace = Boolean(options?.stackReplace); + let selectionIds: string[] = []; - let selectionIds: string[] = Array.isArray(selectedDocumentIds) - ? selectedDocumentIds - .map((id) => (id != null ? String(id) : null)) - .filter((id): id is string => Boolean(id)) - : []; - - if (!stackDocIdsOption && !pointerModifierActive && !wasSelectedAtPointerDown) { + if (Array.isArray(draggedDocIds) && draggedDocIds.length > 0) { + // Use explicitly provided IDs for the drag operation + selectionIds = draggedDocIds; + } else { + // Fallback (should ideally not happen if useDeskPointer is correct) selectionIds = [docKey]; } - if (stackDocIdsOption && stackDocIdsOption.length) { - const selectionSet = new Set(selectionIds); - stackDocIdsOption.forEach((value) => { - if (value != null) { - selectionSet.add(String(value)); - } - }); - selectionIds = Array.from(selectionSet); - } - - const metaOrCtrl = event.metaKey || event.ctrlKey; - if (!stackDocIdsOption && metaOrCtrl && !selectionIds.includes(docKey)) { - selectionIds = [...selectionIds, docKey]; - } - selectionIds = selectionIds.filter((id, index, array) => array.indexOf(id) === index && documentLookup.has(id)); - if (!selectionIds.includes(docKey)) { - selectionIds.unshift(docKey); - } - if (!selectionIds.length) { selectionIds = [docKey]; } @@ -413,7 +371,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { const centerX = Number.isFinite(entry?.centerX) ? entry.centerX : defaultCenterX; const centerY = Number.isFinite(entry?.centerY) ? entry.centerY : defaultCenterY; - const modifierPressed = pointerModifierActive; + const modifierPressed = Boolean(options?.modifierActive); if (!modifierPressed) { if (isGroupDrag) { const layout = layoutRef.current; @@ -498,8 +456,6 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { ? performance.now() : Date.now(); - const hasStackSource = Array.isArray(stackDocIdsOption) && stackDocIdsOption.length > 1; - dragStateRef.current = { docId: docKey, docKey, @@ -532,9 +488,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { activeDocIds: selectionIds, groupItems, groupElevated: !isGroupDrag, - stackDocIds: hasStackSource ? stackDocIdsOption : null, - stackSelectionApplied: stackSelectionAppliedInitial || !hasStackSource, - stackReplace, + stackSelectionApplied: true, massGrams, pointerRadiusScale: 1, lastPointerCanvasX: pointerCanvasX, @@ -595,7 +549,6 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { ensureDocumentSize, layoutRef, resolveBaseMetrics, - selectedDocumentIds, setDraggingId, debugDrag, itemRefs, @@ -690,12 +643,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { state.moved = true; if ( !state.stackSelectionApplied - && Array.isArray(state.stackDocIds) - && state.stackDocIds.length > 0 ) { - safeInvoke(onDocumentStackSelect, state.stackDocIds as Identifier[], event, { - replace: state.stackReplace, - }); state.stackSelectionApplied = true; } if (!state.groupElevated) { @@ -973,7 +921,6 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { layoutRef, itemRefs, debugDrag, - onDocumentStackSelect, setDragTransform, ], ); diff --git a/frontend/src/documents/hooks/useDocumentsSelection.ts b/frontend/src/documents/hooks/useDocumentsSelection.ts index 6c7cfa1..04129df 100644 --- a/frontend/src/documents/hooks/useDocumentsSelection.ts +++ b/frontend/src/documents/hooks/useDocumentsSelection.ts @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; +import type { Dispatch, SetStateAction } from 'react'; import { createDocumentEntryKey, createFolderEntryKey, isFolderEntry } from '../../app/entryKey'; import type { DocumentId, FolderId } from '../../types/identifiers'; @@ -22,8 +23,8 @@ interface UseDocumentsSelectionOptions { showingSearchResults?: boolean; currentSubfolders?: FolderEntry[]; visibleDocuments?: DocumentEntry[]; - configureSelectionEnvironment: (config: { visibleRowKeySet: Set; navigableRowKeys: string[] }) => void; - visibleRowKeySet: Set; + configureSelectionEnvironment: (config: { visibleEntryKeySet: Set; navigableEntryKeys: string[] }) => void; + visibleEntryKeySet: Set; selectedEntries: string[]; selectionAnchorRef: { current: string | null }; promoteSelectionOrderRaw: (id: DocumentId) => void; @@ -31,8 +32,8 @@ interface UseDocumentsSelectionOptions { setActivePreviewId: (id: DocumentId | null) => void; clearSelection: () => void; focusedDocumentId: DocumentId | null; - setFocusedRowKey: (value: string | null | ((current: string | null) => string | null)) => void; - focusedRowKey: string | null; + setFocusedEntryKey: Dispatch>; + focusedEntryKey: string | null; } const useDocumentsSelection = ({ @@ -40,7 +41,7 @@ const useDocumentsSelection = ({ currentSubfolders = [], visibleDocuments = [], configureSelectionEnvironment, - visibleRowKeySet, + visibleEntryKeySet, selectedEntries, selectionAnchorRef, promoteSelectionOrderRaw, @@ -48,8 +49,8 @@ const useDocumentsSelection = ({ setActivePreviewId, clearSelection, focusedDocumentId, - setFocusedRowKey, - focusedRowKey, + setFocusedEntryKey, + focusedEntryKey, }: UseDocumentsSelectionOptions) => { const navigableRows = useMemo(() => { const entries: NavigableRow[] = []; @@ -77,10 +78,10 @@ const useDocumentsSelection = ({ useEffect(() => { configureSelectionEnvironment({ - visibleRowKeySet, - navigableRowKeys, + visibleEntryKeySet, + navigableEntryKeys: navigableRowKeys, }); - }, [configureSelectionEnvironment, visibleRowKeySet, navigableRowKeys]); + }, [configureSelectionEnvironment, visibleEntryKeySet, navigableRowKeys]); const promoteSelectionOrder = useCallback( (docId: DocumentId | null) => { @@ -108,40 +109,40 @@ const useDocumentsSelection = ({ } prevFocusedDocIdRef.current = focusedDocumentId; if (focusedDocumentId) { - setFocusedRowKey(createDocumentEntryKey(focusedDocumentId)); + setFocusedEntryKey(createDocumentEntryKey(focusedDocumentId)); } else { - setFocusedRowKey((current) => (current && isFolderEntry(current) ? current : null)); + setFocusedEntryKey((current) => (current && isFolderEntry(current) ? current : null)); } - }, [focusedDocumentId, setFocusedRowKey]); + }, [focusedDocumentId, setFocusedEntryKey]); useEffect(() => { if (!navigableRowKeys.length) { - if (focusedRowKey) { - setFocusedRowKey(null); + if (focusedEntryKey) { + setFocusedEntryKey(null); } return; } - if (focusedRowKey && navigableRowKeys.includes(focusedRowKey)) { + if (focusedEntryKey && navigableRowKeys.includes(focusedEntryKey)) { return; } const docKey = focusedDocumentId ? createDocumentEntryKey(focusedDocumentId) : null; if (docKey && navigableRowKeys.includes(docKey)) { - setFocusedRowKey(docKey); + setFocusedEntryKey(docKey); return; } const selectedKey = selectedEntries.find((key) => navigableRowKeys.includes(key)); if (selectedKey) { - setFocusedRowKey(selectedKey); + setFocusedEntryKey(selectedKey); return; } - if (focusedRowKey) { - setFocusedRowKey(null); + if (focusedEntryKey) { + setFocusedEntryKey(null); } - }, [focusedRowKey, focusedDocumentId, navigableRowKeys, selectedEntries, setFocusedRowKey]); + }, [focusedEntryKey, focusedDocumentId, navigableRowKeys, selectedEntries, setFocusedEntryKey]); return { navigableRows, diff --git a/frontend/src/documents/panel/DocumentsPanel.tsx b/frontend/src/documents/panel/DocumentsPanel.tsx index 9e075a8..a50e305 100644 --- a/frontend/src/documents/panel/DocumentsPanel.tsx +++ b/frontend/src/documents/panel/DocumentsPanel.tsx @@ -142,8 +142,8 @@ const DocumentsPanelInner: React.FC = ({ }): ReactNode => { const { selectedEntries, - focusedRowKey, - setFocusedRowKey, + focusedEntryKey, + setFocusedEntryKey, handleEntrySelection, clearSelection, selectionAnchorRef, @@ -177,28 +177,28 @@ const DocumentsPanelInner: React.FC = ({ return; } - const rowKeys = docIds + const entryKeys = docIds .map((id) => createDocumentEntryKey(id as Identifier)) .filter((value): value is string => typeof value === 'string'); - if (!rowKeys.length) { + if (!entryKeys.length) { return; } const nextKeys = [...selectedEntries]; - rowKeys.forEach((key) => { + entryKeys.forEach((key) => { if (!nextKeys.includes(key)) { nextKeys.push(key); } }); - const anchor = (rowKeys[0] + const anchor = (entryKeys[0] || selectionAnchorRef.current || nextKeys[nextKeys.length - 1]) as string | null; applySelection(nextKeys, { anchor, - interactedKeys: rowKeys, + interactedKeys: entryKeys, }); }, [applySelection, selectedEntries, selectionAnchorRef], @@ -482,24 +482,24 @@ const DocumentsPanelInner: React.FC = ({ () => entries.map((entry) => ({ key: entry.key, type: entry.type, id: entry.id })), [entries], ); - const navigableRowKeys = useMemo(() => navigableRows.map((row) => row.key), [navigableRows]); + const navigableEntryKeys = useMemo(() => navigableRows.map((row) => row.key), [navigableRows]); const getEntryByKey = useCallback( - (rowKey) => entries.find((entry) => entry.key === rowKey) || null, + (entryKey) => entries.find((entry) => entry.key === entryKey) || null, [entries], ); const handlePanelFocus = useCallback(() => { let resolvedKey = null; - if (focusedRowKey && navigableRowKeys.includes(focusedRowKey)) { - resolvedKey = focusedRowKey; + if (focusedEntryKey && navigableEntryKeys.includes(focusedEntryKey)) { + resolvedKey = focusedEntryKey; } if (!resolvedKey) { for (let index = selectedEntries.length - 1; index >= 0; index -= 1) { const candidate = selectedEntries[index]; - if (navigableRowKeys.includes(candidate)) { + if (navigableEntryKeys.includes(candidate)) { resolvedKey = candidate; break; } @@ -519,12 +519,12 @@ const DocumentsPanelInner: React.FC = ({ return; } - setFocusedRowKey(resolvedKey); + setFocusedEntryKey(resolvedKey); }, [ - focusedRowKey, - navigableRowKeys, + focusedEntryKey, + navigableEntryKeys, navigableRows, - setFocusedRowKey, + setFocusedEntryKey, selectedEntries, ]); @@ -543,15 +543,15 @@ const DocumentsPanelInner: React.FC = ({ event.preventDefault(); let activeKey = - focusedRowKey && navigableRowKeys.includes(focusedRowKey) - ? focusedRowKey + focusedEntryKey && navigableEntryKeys.includes(focusedEntryKey) + ? focusedEntryKey : null; if (!activeKey) { if (selectedEntries.length) { for (let index = selectedEntries.length - 1; index >= 0; index -= 1) { const candidate = selectedEntries[index]; - if (navigableRowKeys.includes(candidate)) { + if (navigableEntryKeys.includes(candidate)) { activeKey = candidate; break; } @@ -559,11 +559,11 @@ const DocumentsPanelInner: React.FC = ({ } if (!activeKey) { - activeKey = key === 'ArrowUp' ? navigableRowKeys[navigableRowKeys.length - 1] : navigableRowKeys[0]; + activeKey = key === 'ArrowUp' ? navigableEntryKeys[navigableEntryKeys.length - 1] : navigableEntryKeys[0]; } } - const currentIndex = navigableRowKeys.indexOf(activeKey); + const currentIndex = navigableEntryKeys.indexOf(activeKey); const activeRow = currentIndex === -1 ? null : navigableRows[currentIndex]; if (key === 'Enter' || key === ' ' || key === 'Space' || key === 'Spacebar') { @@ -601,22 +601,22 @@ const DocumentsPanelInner: React.FC = ({ return; } - setFocusedRowKey(targetRow.key); + setFocusedEntryKey(targetRow.key); handleEntrySelection(targetRow.key, { shiftKey, preventDefault: () => { }, }); }, [ - focusedRowKey, + focusedEntryKey, getEntryByKey, - navigableRowKeys, + navigableEntryKeys, navigableRows, onFolderSelect, selectedEntries, handleDocumentPreviewZoom, handleEntrySelection, - setFocusedRowKey, + setFocusedEntryKey, ], ); @@ -648,14 +648,14 @@ const DocumentsPanelInner: React.FC = ({ const isTagDragEvent = useCallback((event) => isTagTransferEvent(event), []); const ensureFocusedRowVisible = useCallback(() => { - if (!focusedRowKey) return; + if (!focusedEntryKey) return; const container = scrollRef.current; if (!container) return; let selector = null; - if (focusedRowKey.startsWith('document:')) { - selector = `#document-row-${focusedRowKey.slice('document:'.length)}`; - } else if (focusedRowKey.startsWith('folder:')) { - selector = `#folder-row-${focusedRowKey.slice('folder:'.length)}`; + if (focusedEntryKey.startsWith('document:')) { + selector = `#document-row-${focusedEntryKey.slice('document:'.length)}`; + } else if (focusedEntryKey.startsWith('folder:')) { + selector = `#folder-row-${focusedEntryKey.slice('folder:'.length)}`; } if (!selector) { return; @@ -681,22 +681,22 @@ const DocumentsPanelInner: React.FC = ({ const nextScrollTop = rowBottom - container.clientHeight; container.scrollTop = Math.max(nextScrollTop, 0); } - }, [focusedRowKey]); + }, [focusedEntryKey]); useEffect(() => { ensureFocusedRowVisible(); }, [ensureFocusedRowVisible]); const activeDescendantId = useMemo(() => { - if (!focusedRowKey) return undefined; - if (focusedRowKey.startsWith('document:')) { - return `document-row-${focusedRowKey.slice('document:'.length)}`; + if (!focusedEntryKey) return undefined; + if (focusedEntryKey.startsWith('document:')) { + return `document-row-${focusedEntryKey.slice('document:'.length)}`; } - if (focusedRowKey.startsWith('folder:')) { - return `folder-row-${focusedRowKey.slice('folder:'.length)}`; + if (focusedEntryKey.startsWith('folder:')) { + return `folder-row-${focusedEntryKey.slice('folder:'.length)}`; } return undefined; - }, [focusedRowKey]); + }, [focusedEntryKey]); const handleDocumentTagDragOver = useCallback( (event) => { @@ -756,10 +756,10 @@ const DocumentsPanelInner: React.FC = ({ && scrollRef.current ) { scrollRef.current.focus({ preventScroll: true }); - setFocusedRowKey(`folder:${folder.id}`); + setFocusedEntryKey(`folder:${folder.id}`); } }, - [onEntryPointer, setFocusedRowKey], + [onEntryPointer, setFocusedEntryKey], ); const handleDocumentDragStartLocal = useCallback( diff --git a/frontend/src/hooks/documents/useDocumentMutations.ts b/frontend/src/hooks/documents/useDocumentMutations.ts index 713fbc4..edb7779 100644 --- a/frontend/src/hooks/documents/useDocumentMutations.ts +++ b/frontend/src/hooks/documents/useDocumentMutations.ts @@ -123,8 +123,8 @@ interface UseDocumentMutationsArgs { selectionAnchorRef: MutableRefObject; setFocusedDocumentId: Dispatch>; focusedDocumentId: DocumentId | null; - setFocusedRowKey: Dispatch>; - focusedRowKey: string | null; + setFocusedEntryKey: Dispatch>; + focusedEntryKey: string | null; notifyApiError: NotifyApiError; setStatusMessage: SetStatusMessage; mapDocumentCaches: MapDocumentCaches; @@ -197,8 +197,8 @@ const useDocumentMutations = ({ selectionAnchorRef, setFocusedDocumentId, focusedDocumentId, - setFocusedRowKey, - focusedRowKey, + setFocusedEntryKey, + focusedEntryKey, notifyApiError, setStatusMessage, mapDocumentCaches, @@ -357,11 +357,11 @@ const useDocumentMutations = ({ setFocusedDocumentId(null); } if ( - focusedRowKey && - isDocumentEntry(focusedRowKey) && - uniqueIdSet.has(getEntryId(focusedRowKey) as DocumentId) + focusedEntryKey && + isDocumentEntry(focusedEntryKey) && + uniqueIdSet.has(getEntryId(focusedEntryKey) as DocumentId) ) { - setFocusedRowKey(null); + setFocusedEntryKey(null); } } @@ -387,8 +387,8 @@ const useDocumentMutations = ({ selectionAnchorRef, setFocusedDocumentId, focusedDocumentId, - setFocusedRowKey, - focusedRowKey, + setFocusedEntryKey, + focusedEntryKey, notifyApiError, setStatusMessage, mapDocumentCaches, diff --git a/frontend/src/hooks/documents/useDocumentsWorkspace.ts b/frontend/src/hooks/documents/useDocumentsWorkspace.ts index 85519e3..968fbe7 100644 --- a/frontend/src/hooks/documents/useDocumentsWorkspace.ts +++ b/frontend/src/hooks/documents/useDocumentsWorkspace.ts @@ -259,8 +259,8 @@ const useDocumentsWorkspace = ({ selectionInitializedRef, focusedDocumentId, setFocusedDocumentId, - focusedRowKey, - setFocusedRowKey, + focusedEntryKey, + setFocusedEntryKey, applySelection, handleEntrySelection, clearSelection, @@ -416,14 +416,14 @@ const useDocumentsWorkspace = ({ [showingSearchResults, currentSubfolders], ); - const visibleRowKeys = useMemo( + const visibleEntryKeys = useMemo( () => [...visibleFolderKeys, ...visibleDocumentKeys], [visibleFolderKeys, visibleDocumentKeys], ); - const visibleRowKeySet = useMemo( - () => new Set(visibleRowKeys), - [visibleRowKeys], + const visibleEntryKeySet = useMemo( + () => new Set(visibleEntryKeys), + [visibleEntryKeys], ); const { @@ -750,8 +750,8 @@ const useDocumentsWorkspace = ({ selectionAnchorRef, setFocusedDocumentId, focusedDocumentId, - setFocusedRowKey, - focusedRowKey, + setFocusedEntryKey, + focusedEntryKey, notifyApiError, setStatusMessage, mapDocumentCaches, @@ -812,7 +812,7 @@ const useDocumentsWorkspace = ({ currentSubfolders, visibleDocuments: viewDocuments, configureSelectionEnvironment, - visibleRowKeySet, + visibleEntryKeySet, selectedEntries, selectionAnchorRef, promoteSelectionOrderRaw, @@ -820,8 +820,8 @@ const useDocumentsWorkspace = ({ setActivePreviewId, clearSelection, focusedDocumentId, - setFocusedRowKey, - focusedRowKey, + setFocusedEntryKey, + focusedEntryKey, }); const initializeAfterLogin = useCallback(async () => { await Promise.all([refreshTags(), refreshCorrespondents()]);