mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01:00
File reorder logic
This commit is contained in:
parent
7a56f0504e
commit
be037b727f
@ -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<FileId, typeof sourcePages>();
|
||||
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<FileId, PDFPage[]>();
|
||||
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)}
|
||||
>
|
||||
<LoadingOverlay visible={globalProcessing && !mergedPdfDocument} />
|
||||
<LoadingOverlay visible={globalProcessing && !initialDocument} />
|
||||
|
||||
{!mergedPdfDocument && !globalProcessing && selectedFileIds.length === 0 && (
|
||||
{!initialDocument && !globalProcessing && selectedFileIds.length === 0 && (
|
||||
<Center h='100%'>
|
||||
<Stack align="center" gap="md">
|
||||
<Text size="lg" c="dimmed">📄</Text>
|
||||
@ -945,7 +1005,7 @@ const PageEditor = ({
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{!mergedPdfDocument && globalProcessing && (
|
||||
{!initialDocument && globalProcessing && (
|
||||
<Box p={0}>
|
||||
<SkeletonLoader type="controls" />
|
||||
<SkeletonLoader type="pageGrid" count={8} />
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<PDFDocument | null>(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;
|
||||
}
|
||||
@ -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(() => {
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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<FileId[]>([]);
|
||||
|
||||
// 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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -65,7 +65,6 @@ export interface PageEditorFunctions {
|
||||
onExportSelected: () => void;
|
||||
onExportAll: () => void;
|
||||
applyChanges: () => void;
|
||||
reorderPagesByFileOrder: (newFileOrder: FileId[]) => void;
|
||||
exportLoading: boolean;
|
||||
selectionMode: boolean;
|
||||
selectedPageIds: string[];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user