From be037b727f0b8734be1653d359251dec49fdfb9a Mon Sep 17 00:00:00 2001 From: Reece Date: Thu, 23 Oct 2025 18:15:37 +0100 Subject: [PATCH] File reorder logic --- .../src/components/pageEditor/PageEditor.tsx | 306 +++++++++++------- .../pageEditor/commands/pageCommands.ts | 8 +- .../hooks/useInitialPageDocument.ts | 22 ++ .../pageEditor/hooks/usePageDocument.ts | 3 +- .../src/components/shared/TopControls.tsx | 14 +- frontend/src/contexts/PageEditorContext.tsx | 93 +++--- frontend/src/contexts/file/FileReducer.ts | 43 +-- frontend/src/types/pageEditor.ts | 1 - 8 files changed, 274 insertions(+), 216 deletions(-) create mode 100644 frontend/src/components/pageEditor/hooks/useInitialPageDocument.ts diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx index 2f02315ce..6727bc3ea 100644 --- a/frontend/src/components/pageEditor/PageEditor.tsx +++ b/frontend/src/components/pageEditor/PageEditor.tsx @@ -27,6 +27,7 @@ import { PageBreakSettings } from './commands/pageCommands'; import { GRID_CONSTANTS } from './constants'; +import { useInitialPageDocument } from './hooks/useInitialPageDocument'; import { usePageDocument } from './hooks/usePageDocument'; import { usePageEditorState } from './hooks/usePageEditorState'; import { parseSelection } from "../../utils/bulkselection/parseSelection"; @@ -48,7 +49,7 @@ const PageEditor = ({ const { setHasUnsavedChanges } = useNavigationGuard(); // Get PageEditor coordination functions - const { updateFileOrderFromPages, fileOrder } = usePageEditor(); + const { updateFileOrderFromPages, fileOrder, reorderedPages, clearReorderedPages, updateCurrentPages } = usePageEditor(); // Zoom state management const [zoomLevel, setZoomLevel] = useState(1.0); @@ -141,8 +142,103 @@ const PageEditor = ({ const undoManagerRef = useRef(new UndoManager()); // Document state management + // Get initial document ONCE - useInitialPageDocument captures first value only + const initialDocument = useInitialPageDocument(); + + // Also get live mergedPdfDocument for delta sync (source of truth for page existence) const { document: mergedPdfDocument } = usePageDocument(); + // Initialize editedDocument from initial document + useEffect(() => { + if (!initialDocument || editedDocument) return; + + console.log('📄 Initializing editedDocument from initial document:', initialDocument.pages.length, 'pages'); + + // Clone to avoid mutation + setEditedDocument({ + ...initialDocument, + pages: initialDocument.pages.map(p => ({ ...p })) + }); + }, [initialDocument, editedDocument]); + + // Apply file reordering from PageEditorContext + useEffect(() => { + if (reorderedPages && editedDocument) { + setEditedDocument({ + ...editedDocument, + pages: reorderedPages + }); + clearReorderedPages(); + } + }, [reorderedPages, editedDocument, clearReorderedPages]); + + // Live delta sync: reflect external add/remove without touching existing order + useEffect(() => { + if (!mergedPdfDocument || !editedDocument) return; + + const sourcePages = mergedPdfDocument.pages; + const sourceIds = new Set(sourcePages.map(p => p.id)); + + // Group new pages by file (preserve within-file order from source) + const prevIds = new Set(editedDocument.pages.map(p => p.id)); + const newByFile = new Map(); + for (const p of sourcePages) { + if (!prevIds.has(p.id)) { + const fileId = p.originalFileId; + if (!fileId) continue; + const list = newByFile.get(fileId) ?? []; + list.push(p); + newByFile.set(fileId, list); + } + } + + // Fast check: changes exist? + let hasAdditions = newByFile.size > 0; + let hasRemovals = false; + for (const p of editedDocument.pages) { + if (!sourceIds.has(p.id)) { + hasRemovals = true; + break; + } + } + if (!hasAdditions && !hasRemovals) return; + + setEditedDocument(prev => { + if (!prev) return prev; + let pages = [...prev.pages]; + + // Remove pages that no longer exist in source + if (hasRemovals) { + pages = pages.filter(p => sourceIds.has(p.id)); + } + + // Insert new pages while preserving current interleaving + if (hasAdditions) { + // Insert file-by-file at the correct anchors + for (const [, additions] of newByFile) { + // Check if any page has insertAfterPageId (specific insertion point) + const hasSpecificInsertPoint = additions.some(p => (p as any).insertAfterPageId); + + if (hasSpecificInsertPoint) { + // Insert after specific page (ignores file order) + const insertAfterPageId = (additions[0] as any).insertAfterPageId; + const insertAfterIndex = pages.findIndex(p => p.id === insertAfterPageId); + const insertAt = insertAfterIndex >= 0 ? insertAfterIndex + 1 : pages.length; + pages.splice(insertAt, 0, ...additions); + } else { + // Normal add: append to end + pages.push(...additions); + } + } + } + + // Renumber without reordering + pages = pages.map((p, i) => ({ ...p, pageNumber: i + 1 })); + return { ...prev, pages }; + }); + // Only depend on identifiers to avoid loops; do not depend on editedDocument itself + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mergedPdfDocument && mergedPdfDocument.pages.map(p => p.id).join(','), fileOrder.join(',')]); // UI state management const { @@ -209,7 +305,12 @@ const PageEditor = ({ }, []); // Interface functions for parent component - const displayDocument = editedDocument || mergedPdfDocument; + const displayDocument = editedDocument || initialDocument; + + // Feed current pages to PageEditorContext so file reordering can compute page-level changes + useEffect(() => { + updateCurrentPages(displayDocument?.pages ?? null); + }, [displayDocument, updateCurrentPages]); // Derived values for right rail and usePageEditorRightRailButtons (must be after displayDocument) const totalPages = displayDocument?.pages.length || 0; @@ -409,51 +510,8 @@ const PageEditor = ({ executeCommandWithTracking(smartSplitCommand); }, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds, executeCommandWithTracking]); - const handleSplitAll = useCallback(() => { - if (!displayDocument || selectedPageIds.length === 0) return; - - // Convert selected page IDs to page numbers, then to split positions (0-based indices) - const selectedPageNumbers = getPageNumbersFromIds(selectedPageIds); - const selectedPositions: number[] = []; - selectedPageNumbers.forEach(pageNum => { - const pageIndex = displayDocument.pages.findIndex(p => p.pageNumber === pageNum); - if (pageIndex !== -1 && pageIndex < displayDocument.pages.length - 1) { - // Only allow splits before the last page - selectedPositions.push(pageIndex); - } - }); - - if (selectedPositions.length === 0) return; - - // Smart toggle logic: follow the majority, default to adding splits if equal - const existingSplitsCount = selectedPositions.filter(pos => splitPositions.has(pos)).length; - const noSplitsCount = selectedPositions.length - existingSplitsCount; - - // Remove splits only if majority already have splits - // If equal (50/50), default to adding splits - const shouldRemoveSplits = existingSplitsCount > noSplitsCount; - - const newSplitPositions = new Set(splitPositions); - - if (shouldRemoveSplits) { - // Remove splits from all selected positions - selectedPositions.forEach(pos => newSplitPositions.delete(pos)); - } else { - // Add splits to all selected positions - selectedPositions.forEach(pos => newSplitPositions.add(pos)); - } - - // Create a custom command that sets the final state directly - const smartSplitCommand = { - execute: () => setSplitPositions(newSplitPositions), - undo: () => setSplitPositions(splitPositions), - description: shouldRemoveSplits - ? `Remove ${selectedPositions.length} split(s)` - : `Add ${selectedPositions.length - existingSplitsCount} split(s)` - }; - - executeCommandWithTracking(smartSplitCommand); - }, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds, executeCommandWithTracking]); + // Alias for consistency - handleSplitAll is the same as handleSplit (both have smart toggle logic) + const handleSplitAll = handleSplit; const handlePageBreak = useCallback(() => { if (!displayDocument || selectedPageIds.length === 0) return; @@ -469,48 +527,89 @@ const PageEditor = ({ executeCommandWithTracking(pageBreakCommand); }, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]); - const handlePageBreakAll = useCallback(() => { - if (!displayDocument || selectedPageIds.length === 0) return; - - // Convert selected page IDs to page numbers for the command - const selectedPageNumbers = getPageNumbersFromIds(selectedPageIds); - - const pageBreakCommand = new PageBreakCommand( - selectedPageNumbers, - () => displayDocument, - setEditedDocument - ); - executeCommandWithTracking(pageBreakCommand); - }, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]); + // Alias for consistency - handlePageBreakAll is the same as handlePageBreak + const handlePageBreakAll = handlePageBreak; const handleInsertFiles = useCallback(async ( files: File[] | StirlingFileStub[], insertAfterPage: number, isFromStorage?: boolean ) => { - if (!displayDocument || files.length === 0) return; + if (!editedDocument || files.length === 0) return; try { - const targetPage = displayDocument.pages.find(p => p.pageNumber === insertAfterPage); + const targetPage = editedDocument.pages.find(p => p.pageNumber === insertAfterPage); if (!targetPage) return; + console.log('📄 handleInsertFiles: Inserting files after page', insertAfterPage, 'targetPage:', targetPage.id); + + // Add files to FileContext for metadata tracking (without insertAfterPageId) + let addedFileIds: FileId[] = []; if (isFromStorage) { - // Files from storage - use addStirlingFileStubs to avoid re-storing - await actions.addStirlingFileStubs( - files as StirlingFileStub[], - { insertAfterPageId: targetPage.id, selectFiles: true } - ); + const stubs = files as StirlingFileStub[]; + const result = await actions.addStirlingFileStubs(stubs, { selectFiles: true }); + addedFileIds = result.map(f => f.fileId); + console.log('📄 handleInsertFiles: Added stubs, IDs:', addedFileIds); } else { - // New uploaded files - use addFiles - await actions.addFiles( - files as File[], - { insertAfterPageId: targetPage.id, selectFiles: true } - ); + const result = await actions.addFiles(files as File[], { selectFiles: true }); + addedFileIds = result.map(f => f.fileId); + console.log('📄 handleInsertFiles: Added files, IDs:', addedFileIds); + } + + // Wait a moment for files to be processed + await new Promise(resolve => setTimeout(resolve, 100)); + + // Extract pages from newly added files and insert them into editedDocument + const newPages: PDFPage[] = []; + for (const fileId of addedFileIds) { + const stub = selectors.getStirlingFileStub(fileId); + console.log('📄 handleInsertFiles: File', fileId, 'stub:', stub?.name, 'processedFile:', stub?.processedFile?.totalPages, 'pages:', stub?.processedFile?.pages?.length); + if (stub?.processedFile?.pages) { + // Clone pages and ensure proper PDFPage structure + const clonedPages = stub.processedFile.pages.map((page, idx) => ({ + ...page, + id: `${fileId}-${page.pageNumber ?? idx + 1}`, + pageNumber: page.pageNumber ?? idx + 1, + originalFileId: fileId, + originalPageNumber: page.originalPageNumber ?? page.pageNumber ?? idx + 1, + rotation: page.rotation ?? 0, + thumbnail: page.thumbnail ?? null, + selected: false, + splitAfter: page.splitAfter ?? false, + })); + newPages.push(...clonedPages); + } + } + + console.log('📄 handleInsertFiles: Collected', newPages.length, 'new pages'); + + if (newPages.length > 0) { + // Find insertion index in editedDocument + const targetIndex = editedDocument.pages.findIndex(p => p.id === targetPage.id); + console.log('📄 handleInsertFiles: Target index in editedDocument:', targetIndex); + + if (targetIndex >= 0) { + // Clone pages and insert + const updatedPages = [...editedDocument.pages]; + updatedPages.splice(targetIndex + 1, 0, ...newPages); + + // Renumber all pages + updatedPages.forEach((page, index) => { + page.pageNumber = index + 1; + }); + + console.log('📄 handleInsertFiles: Updated pages:', updatedPages.map(p => `${p.id}(${p.pageNumber})`)); + + setEditedDocument({ + ...editedDocument, + pages: updatedPages + }); + } } } catch (error) { console.error('Failed to insert files:', error); } - }, [displayDocument, actions]); + }, [editedDocument, actions, selectors]); const handleSelectAll = useCallback(() => { if (!displayDocument) return; @@ -545,7 +644,7 @@ const PageEditor = ({ selectedPages, () => displayDocument, setEditedDocument, - (newPages) => updateFileOrderFromPages(newPages) + (newPages) => updateFileOrderFromPages(newPages) // Sync file order when pages are reordered ); executeCommandWithTracking(reorderCommand); }, [displayDocument, getPageNumbersFromIds, executeCommandWithTracking, updateFileOrderFromPages]); @@ -597,7 +696,7 @@ const PageEditor = ({ try { // Step 1: Apply DOM changes to document state first const processedDocuments = documentManipulationService.applyDOMChangesToDocument( - mergedPdfDocument || displayDocument, // Original order + displayDocument, // Original order (editedDocument is our working doc now) displayDocument, // Current display order (includes reordering) splitPositions // Position-based splits ); @@ -637,7 +736,7 @@ const PageEditor = ({ console.error('Export failed:', error); setExportLoading(false); } - }, [displayDocument, selectedPageIds, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]); + }, [displayDocument, selectedPageIds, initialDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]); const onExportAll = useCallback(async () => { if (!displayDocument) return; @@ -646,7 +745,7 @@ const PageEditor = ({ try { // Step 1: Apply DOM changes to document state first const processedDocuments = documentManipulationService.applyDOMChangesToDocument( - mergedPdfDocument || displayDocument, + displayDocument, displayDocument, splitPositions ); @@ -683,7 +782,7 @@ const PageEditor = ({ console.error('Export failed:', error); setExportLoading(false); } - }, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]); + }, [displayDocument, initialDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]); // Apply DOM changes to document state using dedicated service const applyChanges = useCallback(async () => { @@ -693,7 +792,7 @@ const PageEditor = ({ try { // Step 1: Apply DOM changes to document state first const processedDocuments = documentManipulationService.applyDOMChangesToDocument( - mergedPdfDocument || displayDocument, + displayDocument, displayDocument, splitPositions ); @@ -718,7 +817,7 @@ const PageEditor = ({ console.error('Apply changes failed:', error); setExportLoading(false); } - }, [displayDocument, mergedPdfDocument, splitPositions, selectedFileIds, getSourceFiles, getExportFilename, actions, selectors, setHasUnsavedChanges]); + }, [displayDocument, initialDocument, splitPositions, selectedFileIds, getSourceFiles, getExportFilename, actions, selectors, setHasUnsavedChanges]); const closePdf = useCallback(() => { @@ -729,44 +828,6 @@ const PageEditor = ({ setSelectionMode(false); }, [actions]); - // Function to reorder pages based on new file order - const reorderPagesByFileOrder = useCallback((newFileOrder: FileId[]) => { - const docToUpdate = editedDocument || mergedPdfDocument; - if (!docToUpdate) return; - - // Group pages by originalFileId - const pagesByFileId = new Map(); - docToUpdate.pages.forEach(page => { - if (page.originalFileId) { - if (!pagesByFileId.has(page.originalFileId)) { - pagesByFileId.set(page.originalFileId, []); - } - pagesByFileId.get(page.originalFileId)!.push(page); - } - }); - - // Rebuild pages array in new file order - const reorderedPages: PDFPage[] = []; - newFileOrder.forEach(fileId => { - const filePages = pagesByFileId.get(fileId); - if (filePages) { - reorderedPages.push(...filePages); - } - }); - - // Renumber pages - const renumberedPages = reorderedPages.map((page, idx) => ({ - ...page, - pageNumber: idx + 1 - })); - - setEditedDocument({ - ...docToUpdate, - pages: renumberedPages, - totalPages: renumberedPages.length - }); - }, [editedDocument, mergedPdfDocument]); - usePageEditorRightRailButtons({ totalPages, selectedPageCount, @@ -818,7 +879,6 @@ const PageEditor = ({ onExportSelected, onExportAll, applyChanges, - reorderPagesByFileOrder, exportLoading, selectionMode, selectedPageIds, @@ -830,7 +890,7 @@ const PageEditor = ({ } }, [ onFunctionsReady, handleUndo, handleRedo, canUndo, canRedo, handleRotate, handleDelete, handleSplit, handleSplitAll, - handlePageBreak, handlePageBreakAll, handleSelectAll, handleDeselectAll, handleSetSelectedPages, handleExportPreview, onExportSelected, onExportAll, applyChanges, reorderPagesByFileOrder, exportLoading, + handlePageBreak, handlePageBreakAll, handleSelectAll, handleDeselectAll, handleSetSelectedPages, handleExportPreview, onExportSelected, onExportAll, applyChanges, exportLoading, selectionMode, selectedPageIds, splitPositions, displayDocument?.pages.length, closePdf ]); @@ -933,9 +993,9 @@ const PageEditor = ({ onMouseEnter={() => setIsContainerHovered(true)} onMouseLeave={() => setIsContainerHovered(false)} > - + - {!mergedPdfDocument && !globalProcessing && selectedFileIds.length === 0 && ( + {!initialDocument && !globalProcessing && selectedFileIds.length === 0 && (
📄 @@ -945,7 +1005,7 @@ const PageEditor = ({
)} - {!mergedPdfDocument && globalProcessing && ( + {!initialDocument && globalProcessing && ( diff --git a/frontend/src/components/pageEditor/commands/pageCommands.ts b/frontend/src/components/pageEditor/commands/pageCommands.ts index 4d5b57e16..63a4bf567 100644 --- a/frontend/src/components/pageEditor/commands/pageCommands.ts +++ b/frontend/src/components/pageEditor/commands/pageCommands.ts @@ -197,7 +197,13 @@ export class ReorderPagesCommand extends DOMCommand { } else { // Single page reorder const [movedPage] = newPages.splice(sourceIndex, 1); - newPages.splice(this.targetIndex, 0, movedPage); + + // Adjust target index if moving forward (after removal, indices shift) + const adjustedTargetIndex = sourceIndex < this.targetIndex + ? this.targetIndex - 1 + : this.targetIndex; + + newPages.splice(adjustedTargetIndex, 0, movedPage); newPages.forEach((page, index) => { page.pageNumber = index + 1; diff --git a/frontend/src/components/pageEditor/hooks/useInitialPageDocument.ts b/frontend/src/components/pageEditor/hooks/useInitialPageDocument.ts new file mode 100644 index 000000000..874fdda43 --- /dev/null +++ b/frontend/src/components/pageEditor/hooks/useInitialPageDocument.ts @@ -0,0 +1,22 @@ +import { useState, useEffect } from 'react'; +import { usePageDocument } from './usePageDocument'; +import { PDFDocument } from '../../../types/pageEditor'; + +/** + * Hook that calls usePageDocument but only returns the FIRST non-null result + * After initialization, it ignores all subsequent updates + */ +export function useInitialPageDocument(): PDFDocument | null { + const { document: liveDocument } = usePageDocument(); + const [initialDocument, setInitialDocument] = useState(null); + + useEffect(() => { + // Only set once when we get the first non-null document + if (liveDocument && !initialDocument) { + console.log('📄 useInitialPageDocument: Captured initial document with', liveDocument.pages.length, 'pages'); + setInitialDocument(liveDocument); + } + }, [liveDocument, initialDocument]); + + return initialDocument; +} diff --git a/frontend/src/components/pageEditor/hooks/usePageDocument.ts b/frontend/src/components/pageEditor/hooks/usePageDocument.ts index 0f0943d3e..24970973c 100644 --- a/frontend/src/components/pageEditor/hooks/usePageDocument.ts +++ b/frontend/src/components/pageEditor/hooks/usePageDocument.ts @@ -26,6 +26,7 @@ export function usePageDocument(): PageDocumentHook { // Filter to only include PDF files (PageEditor only supports PDFs) // Use stable string keys to prevent infinite loops const allFileIdsKey = allFileIds.join(','); + const selectedFileIdsKey = [...state.ui.selectedFileIds].sort().join(','); const activeFilesSignature = selectors.getFilesSignature(); // Get ALL PDF files (selected or not) for document building with placeholders @@ -192,7 +193,7 @@ export function usePageDocument(): PageDocumentHook { }; return mergedDoc; - }, [activeFileIds, primaryFileId, primaryStirlingFileStub, processedFilePages, processedFileTotalPages, selectors, activeFilesSignature]); + }, [activeFileIds, primaryFileId, primaryStirlingFileStub, processedFilePages, processedFileTotalPages, selectors, activeFilesSignature, selectedFileIdsKey, state.ui.selectedFileIds]); // Large document detection for smart loading const isVeryLargeDocument = useMemo(() => { diff --git a/frontend/src/components/shared/TopControls.tsx b/frontend/src/components/shared/TopControls.tsx index e64ca21c8..d01a402c8 100644 --- a/frontend/src/components/shared/TopControls.tsx +++ b/frontend/src/components/shared/TopControls.tsx @@ -275,19 +275,9 @@ const TopControls = ({ // Memoize the reorder handler const handleReorder = useCallback((fromIndex: number, toIndex: number) => { - // Reorder files in PageEditorContext (updates fileOrder) + // Single source of truth: PageEditorContext handles file->page reorder propagation pageEditorReorderFiles(fromIndex, toIndex); - - // Also reorder pages directly - const newOrder = [...pageEditorFileOrder]; - const [movedFileId] = newOrder.splice(fromIndex, 1); - newOrder.splice(toIndex, 0, movedFileId); - - // Call reorderPagesByFileOrder if available - if (pageEditorFunctions?.reorderPagesByFileOrder) { - pageEditorFunctions.reorderPagesByFileOrder(newOrder); - } - }, [pageEditorReorderFiles, pageEditorFileOrder, pageEditorFunctions]); + }, [pageEditorReorderFiles]); const handleViewChange = useCallback((view: string) => { if (!isValidWorkbench(view)) { diff --git a/frontend/src/contexts/PageEditorContext.tsx b/frontend/src/contexts/PageEditorContext.tsx index 8ac48fb27..4c488ec8d 100644 --- a/frontend/src/contexts/PageEditorContext.tsx +++ b/frontend/src/contexts/PageEditorContext.tsx @@ -67,9 +67,12 @@ function reorderPagesForFileMove( let insertionIndex = 0; if (fromIndex < toIndex) { - // Moving down: insert AFTER the last page of target file + // Moving down: insert AFTER the last page of ANY file that should come before us + // We need to find the last page belonging to any file at index <= toIndex in orderedFileIds + const filesBeforeUs = new Set(orderedFileIds.slice(0, toIndex + 1)); for (let i = remainingPages.length - 1; i >= 0; i--) { - if (remainingPages[i].originalFileId === targetFileId) { + const pageFileId = remainingPages[i].originalFileId; + if (pageFileId && filesBeforeUs.has(pageFileId)) { insertionIndex = i + 1; break; } @@ -151,51 +154,56 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) { stateRef.current = state; }, [state]); + // Track the previous FileContext order to detect actual changes + const prevFileContextIdsRef = React.useRef([]); + // Initialize fileOrder from FileContext when files change (add/remove only) React.useEffect(() => { const currentFileIds = state.files.ids; + const prevFileIds = prevFileContextIdsRef.current; - // Identify new files - const newFileIds = currentFileIds.filter(id => !fileOrder.includes(id)); + // Only react to FileContext changes, not our own fileOrder changes + const fileContextChanged = + currentFileIds.length !== prevFileIds.length || + !currentFileIds.every((id, idx) => id === prevFileIds[idx]); - // Remove deleted files - const validFileOrder = fileOrder.filter(id => currentFileIds.includes(id)); + if (!fileContextChanged) { + return; + } - if (newFileIds.length > 0 || validFileOrder.length !== fileOrder.length) { - // Check if new files have insertion positions - let hasInsertionPosition = false; - for (const fileId of newFileIds) { + prevFileContextIdsRef.current = currentFileIds; + + // Collect new file IDs outside the setState callback so we can clear them after + let newFileIdsToProcess: FileId[] = []; + + // Use functional setState to read latest fileOrder without depending on it + setFileOrder(currentOrder => { + // Identify new files + const newFileIds = currentFileIds.filter(id => !currentOrder.includes(id)); + newFileIdsToProcess = newFileIds; // Store for cleanup + + // Remove deleted files + const validFileOrder = currentOrder.filter(id => currentFileIds.includes(id)); + + if (newFileIds.length === 0 && validFileOrder.length === currentOrder.length) { + return currentOrder; // No changes needed + } + + // Always append new files to end + // If files have insertAfterPageId, page-level insertion is handled by usePageDocument + return [...validFileOrder, ...newFileIds]; + }); + + // Clear insertAfterPageId after a delay to allow usePageDocument to consume it first + setTimeout(() => { + newFileIdsToProcess.forEach(fileId => { const stub = state.files.byId[fileId]; if (stub?.insertAfterPageId) { - hasInsertionPosition = true; - break; + fileActions.updateStirlingFileStub(fileId, { insertAfterPageId: undefined }); } - } - - if (hasInsertionPosition) { - // Respect FileContext order when files have insertion positions - // FileContext already handled the positioning logic - const orderedNewFiles = currentFileIds.filter(id => newFileIds.includes(id)); - const orderedValidFiles = currentFileIds.filter(id => validFileOrder.includes(id)); - - // Merge while preserving FileContext order - const newOrder: FileId[] = []; - const newFilesSet = new Set(orderedNewFiles); - const validFilesSet = new Set(orderedValidFiles); - - currentFileIds.forEach(id => { - if (newFilesSet.has(id) || validFilesSet.has(id)) { - newOrder.push(id); - } - }); - - setFileOrder(newOrder); - } else { - // No insertion positions - append new files to end - setFileOrder([...validFileOrder, ...newFileIds]); - } - } - }, [state.files.ids, state.files.byId, fileOrder]); + }); + }, 100); + }, [state.files.ids, state.files.byId, fileActions]); const updateCurrentPages = useCallback((pages: PDFPage[] | null) => { setCurrentPages(pages); @@ -276,9 +284,12 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) { } }); - // Get the moved and target file IDs - const movedFileId = fileOrder[fromIndex]; - const targetFileId = fileOrder[toIndex]; + // Get the target file ID from the NEW order (after the move) + // When moving down: we want to position after the file at toIndex-1 (file just before insertion) + // When moving up: we want to position before the file at toIndex+1 (file just after insertion) + const targetFileId = fromIndex < toIndex + ? newOrder[toIndex - 1] // Moving down: target is the file just before where we inserted + : newOrder[toIndex + 1]; // Moving up: target is the file just after where we inserted // Find their positions in the current page order (not the full file list) const pageOrderFromIndex = currentFileOrder.findIndex(id => id === movedFileId); diff --git a/frontend/src/contexts/file/FileReducer.ts b/frontend/src/contexts/file/FileReducer.ts index 0a01c4ec4..b4c6646b0 100644 --- a/frontend/src/contexts/file/FileReducer.ts +++ b/frontend/src/contexts/file/FileReducer.ts @@ -89,47 +89,16 @@ export function fileContextReducer(state: FileContextState, action: FileContextA insertAfterPageId = record.insertAfterPageId; } - // Store record but clear insertAfterPageId (it's only used once) - const { insertAfterPageId: _, ...recordWithoutInsertPosition } = record; - newById[record.id] = recordWithoutInsertPosition; + // Store record WITH insertAfterPageId temporarily + // PageEditorContext will read it and clear it + newById[record.id] = record; } }); // Determine final file order - let finalIds: FileId[]; - - if (hasInsertionPosition && insertAfterPageId) { - // Find the file that contains the page with insertAfterPageId - let insertIndex = state.files.ids.length; // Default to end - - for (let i = 0; i < state.files.ids.length; i++) { - const fileId = state.files.ids[i]; - const fileStub = state.files.byId[fileId]; - - if (fileStub?.processedFile?.pages) { - const hasPage = fileStub.processedFile.pages.some(page => { - // Page ID format: fileId-pageNumber - const pageId = `${fileId}-${page.pageNumber}`; - return pageId === insertAfterPageId; - }); - - if (hasPage) { - insertIndex = i + 1; // Insert after this file - break; - } - } - } - - // Insert new files at the calculated position - finalIds = [ - ...state.files.ids.slice(0, insertIndex), - ...newIds, - ...state.files.ids.slice(insertIndex) - ]; - } else { - // No insertion position - append to end - finalIds = [...state.files.ids, ...newIds]; - } + // NOTE: If files have insertAfterPageId, we just append to end + // The page-level insertion is handled by usePageDocument + const finalIds = [...state.files.ids, ...newIds]; // Auto-select inserted files const newSelectedFileIds = hasInsertionPosition diff --git a/frontend/src/types/pageEditor.ts b/frontend/src/types/pageEditor.ts index 84e69fb0e..a388a0578 100644 --- a/frontend/src/types/pageEditor.ts +++ b/frontend/src/types/pageEditor.ts @@ -65,7 +65,6 @@ export interface PageEditorFunctions { onExportSelected: () => void; onExportAll: () => void; applyChanges: () => void; - reorderPagesByFileOrder: (newFileOrder: FileId[]) => void; exportLoading: boolean; selectionMode: boolean; selectedPageIds: string[];