Working mostly

This commit is contained in:
Reece 2025-10-15 21:33:54 +01:00
parent e7c6db082c
commit 74e8388bce
10 changed files with 393 additions and 56 deletions

View File

@ -6,6 +6,7 @@ import { GRID_CONSTANTS } from './constants';
interface DragDropItem {
id: string;
splitAfter?: boolean;
isPlaceholder?: boolean;
}
interface DragDropGridProps<T extends DragDropItem> {
@ -25,9 +26,12 @@ const DragDropGrid = <T extends DragDropItem>({
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
const containerRef = useRef<HTMLDivElement>(null);
// Filter out placeholder items (invisible pages for deselected files)
const visibleItems = items.filter(item => !item.isPlaceholder);
// Responsive grid configuration
const [itemsPerRow, setItemsPerRow] = useState(4);
const OVERSCAN = items.length > 1000 ? GRID_CONSTANTS.OVERSCAN_LARGE : GRID_CONSTANTS.OVERSCAN_SMALL;
const OVERSCAN = visibleItems.length > 1000 ? GRID_CONSTANTS.OVERSCAN_LARGE : GRID_CONSTANTS.OVERSCAN_SMALL;
// Calculate items per row based on container width
const calculateItemsPerRow = useCallback(() => {
@ -76,7 +80,7 @@ const DragDropGrid = <T extends DragDropItem>({
// Virtualization with react-virtual library
const rowVirtualizer = useVirtualizer({
count: Math.ceil(items.length / itemsPerRow),
count: Math.ceil(visibleItems.length / itemsPerRow),
getScrollElement: () => containerRef.current?.closest('[data-scrolling-container]') as Element,
estimateSize: () => {
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
@ -111,8 +115,8 @@ const DragDropGrid = <T extends DragDropItem>({
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const startIndex = virtualRow.index * itemsPerRow;
const endIndex = Math.min(startIndex + itemsPerRow, items.length);
const rowItems = items.slice(startIndex, endIndex);
const endIndex = Math.min(startIndex + itemsPerRow, visibleItems.length);
const rowItems = visibleItems.slice(startIndex, endIndex);
return (
<div

View File

@ -0,0 +1,86 @@
import React, { useState } from 'react';
import { Modal, Button, Select, Radio, Group, Stack } from '@mantine/core';
export type PageSize = 'A4' | 'Letter' | 'Legal' | 'A3' | 'A5';
export type PageOrientation = 'portrait' | 'landscape';
export interface PageBreakSettings {
size: PageSize;
orientation: PageOrientation;
}
interface PageBreakSettingsModalProps {
opened: boolean;
onClose: () => void;
onConfirm: (settings: PageBreakSettings) => void;
selectedPageCount: number;
}
const PAGE_SIZES: { value: PageSize; label: string; dimensions: string }[] = [
{ value: 'A4', label: 'A4', dimensions: '210 × 297 mm' },
{ value: 'Letter', label: 'Letter', dimensions: '8.5 × 11 in' },
{ value: 'Legal', label: 'Legal', dimensions: '8.5 × 14 in' },
{ value: 'A3', label: 'A3', dimensions: '297 × 420 mm' },
{ value: 'A5', label: 'A5', dimensions: '148 × 210 mm' },
];
export const PageBreakSettingsModal: React.FC<PageBreakSettingsModalProps> = ({
opened,
onClose,
onConfirm,
selectedPageCount,
}) => {
const [size, setSize] = useState<PageSize>('A4');
const [orientation, setOrientation] = useState<PageOrientation>('portrait');
const handleConfirm = () => {
onConfirm({ size, orientation });
onClose();
};
return (
<Modal
opened={opened}
onClose={onClose}
title={`Insert ${selectedPageCount} Page Break${selectedPageCount > 1 ? 's' : ''}`}
centered
size="md"
>
<Stack gap="md">
<Select
label="Page Size"
value={size}
onChange={(value) => setSize(value as PageSize)}
data={PAGE_SIZES.map(ps => ({
value: ps.value,
label: `${ps.label} (${ps.dimensions})`
}))}
/>
<div>
<div style={{ marginBottom: '0.5rem', fontSize: '0.875rem', fontWeight: 500 }}>
Orientation
</div>
<Radio.Group
value={orientation}
onChange={(value) => setOrientation(value as PageOrientation)}
>
<Group gap="md">
<Radio value="portrait" label="Portrait" />
<Radio value="landscape" label="Landscape" />
</Group>
</Radio.Group>
</div>
<Group justify="flex-end" mt="md">
<Button variant="subtle" onClick={onClose}>
Cancel
</Button>
<Button onClick={handleConfirm}>
Insert Page Break{selectedPageCount > 1 ? 's' : ''}
</Button>
</Group>
</Stack>
</Modal>
);
};

View File

@ -22,11 +22,13 @@ import {
SplitCommand,
BulkRotateCommand,
PageBreakCommand,
UndoManager
UndoManager,
PageBreakSettings
} from './commands/pageCommands';
import { GRID_CONSTANTS } from './constants';
import { usePageDocument } from './hooks/usePageDocument';
import { usePageEditorState } from './hooks/usePageEditorState';
import { PageBreakSettingsModal } from './PageBreakSettingsModal';
export interface PageEditorProps {
onFunctionsReady?: (functions: PageEditorFunctions) => void;
@ -44,13 +46,13 @@ const PageEditor = ({
const { setHasUnsavedChanges } = useNavigationGuard();
// Get PageEditor coordination functions
const { updateCurrentPages, reorderedPages, clearReorderedPages, updateFileOrderFromPages } = usePageEditor();
const { updateCurrentPages, reorderedPages, clearReorderedPages, updateFileOrderFromPages, fileOrder } = usePageEditor();
// Derive page editor files from FileContext (single source of truth)
// Derive page editor files from PageEditorContext's fileOrder (page editor workspace order)
// Filter to only show PDF files (PageEditor only supports PDFs)
// Use stable string keys to prevent infinite loops
// Cache file objects to prevent infinite re-renders from new object references
const fileIdsKey = state.files.ids.join(',');
const fileOrderKey = fileOrder.join(',');
const selectedIdsKey = [...state.ui.selectedFileIds].sort().join(',');
const filesSignature = selectors.getFilesSignature();
@ -60,7 +62,7 @@ const PageEditor = ({
const cache = fileObjectsRef.current;
const newFiles: any[] = [];
state.files.ids.forEach(fileId => {
fileOrder.forEach(fileId => {
const stub = selectors.getStirlingFileStub(fileId);
const isSelected = state.ui.selectedFileIds.includes(fileId);
const isPdf = stub?.name?.toLowerCase().endsWith('.pdf') ?? false;
@ -100,10 +102,15 @@ const PageEditor = ({
return newFiles;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fileIdsKey, selectedIdsKey, filesSignature]);
}, [fileOrderKey, selectedIdsKey, filesSignature]);
// Get active file IDs from SELECTED files only
const activeFileIds = useMemo(() => {
// Get ALL file IDs in order (not filtered by selection)
const orderedFileIds = useMemo(() => {
return pageEditorFiles.map(f => f.fileId);
}, [pageEditorFiles]);
// Get selected file IDs for filtering
const selectedFileIds = useMemo(() => {
return pageEditorFiles.filter(f => f.isSelected).map(f => f.fileId);
}, [pageEditorFiles]);
@ -137,6 +144,10 @@ const PageEditor = ({
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
// Page break modal state
const [pageBreakModalOpened, setPageBreakModalOpened] = useState(false);
const [pageBreakModalPageCount, setPageBreakModalPageCount] = useState(0);
// Update undo/redo state
const updateUndoRedoState = useCallback(() => {
setCanUndo(undoManagerRef.current.canUndo());
@ -202,12 +213,89 @@ const PageEditor = ({
}
}, [reorderedPages, displayDocument, clearReorderedPages]);
// Update file order when pages are manually reordered
// 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]);
// When file selection changes, update placeholder flags in editedDocument
const prevSelectedFileIdsRef = useRef<FileId[]>([]);
useEffect(() => {
if (editedDocument?.pages && editedDocument.pages.length > 0 && activeFileIds.length > 1) {
updateFileOrderFromPages(editedDocument.pages);
const prevIds = prevSelectedFileIdsRef.current;
const currentIds = selectedFileIds;
// Skip if no change or first render
if (prevIds.length === 0 && currentIds.length > 0) {
prevSelectedFileIdsRef.current = currentIds;
return;
}
}, [editedDocument?.pages, activeFileIds.length, updateFileOrderFromPages]);
if (prevIds.length === currentIds.length && prevIds.every((id, idx) => id === currentIds[idx])) {
return;
}
// Use editedDocument if available, otherwise use mergedPdfDocument
const docToUpdate = editedDocument || mergedPdfDocument;
if (!docToUpdate) {
prevSelectedFileIdsRef.current = currentIds;
return;
}
const currentSet = new Set(currentIds);
// Update placeholder flags on existing pages based on selection
const updatedPages = docToUpdate.pages.map(page => {
const isSelected = page.originalFileId ? currentSet.has(page.originalFileId) : true;
// If file is deselected, mark all its pages as placeholders
// If file is selected, remove placeholder flag
if (!isSelected) {
return { ...page, isPlaceholder: true };
} else {
return { ...page, isPlaceholder: false };
}
});
setEditedDocument({
...docToUpdate,
pages: updatedPages
});
prevSelectedFileIdsRef.current = currentIds;
}, [selectedFileIds, editedDocument, mergedPdfDocument]);
// Utility functions to convert between page IDs and page numbers
const getPageNumbersFromIds = useCallback((pageIds: string[]): number[] => {
@ -451,13 +539,22 @@ const PageEditor = ({
const handlePageBreak = useCallback(() => {
if (!displayDocument || selectedPageIds.length === 0) return;
// Show modal to get page break settings
setPageBreakModalPageCount(selectedPageIds.length);
setPageBreakModalOpened(true);
}, [selectedPageIds, displayDocument]);
const handlePageBreakConfirm = useCallback((settings: PageBreakSettings) => {
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
setEditedDocument,
settings
);
executeCommandWithTracking(pageBreakCommand);
}, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
@ -515,17 +612,18 @@ const PageEditor = ({
targetIndex,
selectedPages,
() => displayDocument,
setEditedDocument
setEditedDocument,
(newPages) => updateFileOrderFromPages(newPages)
);
executeCommandWithTracking(reorderCommand);
}, [displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
}, [displayDocument, getPageNumbersFromIds, executeCommandWithTracking, updateFileOrderFromPages]);
// Helper function to collect source files for multi-file export
const getSourceFiles = useCallback((): Map<FileId, File> | null => {
const sourceFiles = new Map<FileId, File>();
// Always include original files
activeFileIds.forEach(fileId => {
// Always include selected files
selectedFileIds.forEach(fileId => {
const file = selectors.getFile(fileId);
if (file) {
sourceFiles.set(fileId, file);
@ -534,31 +632,31 @@ const PageEditor = ({
// Use multi-file export if we have multiple original files
const hasInsertedFiles = false;
const hasMultipleOriginalFiles = activeFileIds.length > 1;
const hasMultipleOriginalFiles = selectedFileIds.length > 1;
if (!hasInsertedFiles && !hasMultipleOriginalFiles) {
return null; // Use single-file export method
}
return sourceFiles.size > 0 ? sourceFiles : null;
}, [activeFileIds, selectors]);
}, [selectedFileIds, selectors]);
// Helper function to generate proper filename for exports
const getExportFilename = useCallback((): string => {
if (activeFileIds.length <= 1) {
if (selectedFileIds.length <= 1) {
// Single file - use original name
return displayDocument?.name || 'document.pdf';
}
// Multiple files - use first file name with " (merged)" suffix
const firstFile = selectors.getFile(activeFileIds[0]);
const firstFile = selectors.getFile(selectedFileIds[0]);
if (firstFile) {
const baseName = firstFile.name.replace(/\.pdf$/i, '');
return `${baseName} (merged).pdf`;
}
return 'merged-document.pdf';
}, [activeFileIds, selectors, displayDocument]);
}, [selectedFileIds, selectors, displayDocument]);
const onExportSelected = useCallback(async () => {
if (!displayDocument || selectedPageIds.length === 0) return;
@ -674,13 +772,13 @@ const PageEditor = ({
const files = await exportProcessedDocumentsToFiles(processedDocuments, sourceFiles, exportFilename);
// Step 3: Create StirlingFiles and stubs for version history
const parentStub = selectors.getStirlingFileStub(activeFileIds[0]);
const parentStub = selectors.getStirlingFileStub(selectedFileIds[0]);
if (!parentStub) throw new Error('Parent stub not found');
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs(files, parentStub, 'multiTool');
// Step 4: Consume files (replace in context)
await actions.consumeFiles(activeFileIds, stirlingFiles, stubs);
await actions.consumeFiles(selectedFileIds, stirlingFiles, stubs);
setHasUnsavedChanges(false);
setExportLoading(false);
@ -688,7 +786,7 @@ const PageEditor = ({
console.error('Apply changes failed:', error);
setExportLoading(false);
}
}, [displayDocument, mergedPdfDocument, splitPositions, activeFileIds, getSourceFiles, getExportFilename, actions, selectors, setHasUnsavedChanges]);
}, [displayDocument, mergedPdfDocument, splitPositions, selectedFileIds, getSourceFiles, getExportFilename, actions, selectors, setHasUnsavedChanges]);
const closePdf = useCallback(() => {
@ -733,6 +831,7 @@ const PageEditor = ({
onExportSelected,
onExportAll,
applyChanges,
reorderPagesByFileOrder,
exportLoading,
selectionMode,
selectedPageIds,
@ -744,7 +843,7 @@ const PageEditor = ({
}
}, [
onFunctionsReady, handleUndo, handleRedo, canUndo, canRedo, handleRotate, handleDelete, handleSplit, handleSplitAll,
handlePageBreak, handlePageBreakAll, handleSelectAll, handleDeselectAll, handleSetSelectedPages, handleExportPreview, onExportSelected, onExportAll, applyChanges, exportLoading,
handlePageBreak, handlePageBreakAll, handleSelectAll, handleDeselectAll, handleSetSelectedPages, handleExportPreview, onExportSelected, onExportAll, applyChanges, reorderPagesByFileOrder, exportLoading,
selectionMode, selectedPageIds, splitPositions, displayDocument?.pages.length, closePdf
]);
@ -757,28 +856,28 @@ const PageEditor = ({
// Create a stable mapping of fileId to color index (preserves colors on reorder)
const fileColorIndexMap = useMemo(() => {
// Assign colors to new files based on insertion order
activeFileIds.forEach(fileId => {
orderedFileIds.forEach(fileId => {
if (!fileColorAssignments.current.has(fileId)) {
fileColorAssignments.current.set(fileId, fileColorAssignments.current.size);
}
});
// Clean up removed files
const activeSet = new Set(activeFileIds);
// Clean up removed files (only remove files that are completely gone, not just deselected)
const allFilesSet = new Set(orderedFileIds);
for (const fileId of fileColorAssignments.current.keys()) {
if (!activeSet.has(fileId)) {
if (!allFilesSet.has(fileId)) {
fileColorAssignments.current.delete(fileId);
}
}
return fileColorAssignments.current;
}, [activeFileIds.join(',')]); // Only recalculate when the set of files changes, not the order
}, [orderedFileIds.join(',')]); // Only recalculate when the set of files changes, not the order
return (
<Box pos="relative" h='100%' style={{ overflow: 'auto' }} data-scrolling-container="true">
<LoadingOverlay visible={globalProcessing && !mergedPdfDocument} />
{!mergedPdfDocument && !globalProcessing && activeFileIds.length === 0 && (
{!mergedPdfDocument && !globalProcessing && selectedFileIds.length === 0 && (
<Center h='100%'>
<Stack align="center" gap="md">
<Text size="lg" c="dimmed">📄</Text>
@ -908,6 +1007,13 @@ const PageEditor = ({
await onExportAll();
}}
/>
<PageBreakSettingsModal
opened={pageBreakModalOpened}
onClose={() => setPageBreakModalOpened(false)}
onConfirm={handlePageBreakConfirm}
selectedPageCount={pageBreakModalPageCount}
/>
</Box>
);
};

View File

@ -281,7 +281,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
setMouseStartPos(null);
}, []);
const fileColorBorder = getFileColorWithOpacity(fileColorIndex, 0.3);
const fileColorBorder = page.isBlankPage ? 'transparent' : getFileColorWithOpacity(fileColorIndex, 0.3);
return (
<div
@ -410,7 +410,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
position: 'absolute',
top: 5,
left: 5,
background: page.isBlankPage ? 'rgba(255, 165, 0, 0.8)' : 'rgba(162, 201, 255, 0.8)',
background: 'rgba(162, 201, 255, 0.8)',
padding: '6px 8px',
borderRadius: 8,
zIndex: 2,

View File

@ -161,7 +161,8 @@ export class ReorderPagesCommand extends DOMCommand {
private targetIndex: number,
private selectedPages: number[] | undefined,
private getCurrentDocument: () => PDFDocument | null,
private setDocument: (doc: PDFDocument) => void
private setDocument: (doc: PDFDocument) => void,
private onReorderComplete?: (newPages: PDFPage[]) => void
) {
super();
}
@ -210,6 +211,11 @@ export class ReorderPagesCommand extends DOMCommand {
};
this.setDocument(reorderedDocument);
// Notify that reordering is complete
if (this.onReorderComplete) {
this.onReorderComplete(newPages);
}
}
undo(): void {
@ -408,6 +414,14 @@ export class SplitAllCommand extends DOMCommand {
}
}
export type PageSize = 'A4' | 'Letter' | 'Legal' | 'A3' | 'A5';
export type PageOrientation = 'portrait' | 'landscape';
export interface PageBreakSettings {
size: PageSize;
orientation: PageOrientation;
}
export class PageBreakCommand extends DOMCommand {
private insertedPages: PDFPage[] = [];
private originalDocument: PDFDocument | null = null;
@ -415,7 +429,8 @@ export class PageBreakCommand extends DOMCommand {
constructor(
private selectedPageNumbers: number[],
private getCurrentDocument: () => PDFDocument | null,
private setDocument: (doc: PDFDocument) => void
private setDocument: (doc: PDFDocument) => void,
private settings?: PageBreakSettings
) {
super();
}
@ -450,7 +465,8 @@ export class PageBreakCommand extends DOMCommand {
rotation: 0,
selected: false,
splitAfter: false,
isBlankPage: true // Custom flag for blank pages
isBlankPage: true, // Custom flag for blank pages
pageBreakSettings: this.settings // Store settings for export
};
newPages.push(blankPage);
this.insertedPages.push(blankPage);

View File

@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { useFileState } from '../../../contexts/FileContext';
import { usePageEditor } from '../../../contexts/PageEditorContext';
import { PDFDocument, PDFPage } from '../../../types/pageEditor';
import { FileId } from '../../../types/file';
@ -15,9 +16,11 @@ export interface PageDocumentHook {
*/
export function usePageDocument(): PageDocumentHook {
const { state, selectors } = useFileState();
const { fileOrder } = usePageEditor();
// Convert Set to array and filter to maintain file order from FileContext
const allFileIds = state.files.ids;
// Use PageEditorContext's fileOrder instead of FileContext's global order
// This ensures the page editor respects its own workspace ordering
const allFileIds = fileOrder;
// Derive selected file IDs directly from FileContext (single source of truth)
// Filter to only include PDF files (PageEditor only supports PDFs)
@ -26,15 +29,14 @@ export function usePageDocument(): PageDocumentHook {
const selectedIdsKey = [...state.ui.selectedFileIds].sort().join(',');
const filesSignature = selectors.getFilesSignature();
// Get ALL PDF files (selected or not) for document building with placeholders
const activeFileIds = useMemo(() => {
const selectedFileIds = new Set(state.ui.selectedFileIds);
return allFileIds.filter(id => {
if (!selectedFileIds.has(id)) return false;
const stub = selectors.getStirlingFileStub(id);
return stub?.name?.toLowerCase().endsWith('.pdf') ?? false;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allFileIdsKey, selectedIdsKey, filesSignature]);
}, [allFileIdsKey, filesSignature]);
const primaryFileId = activeFileIds[0] ?? null;
@ -84,13 +86,28 @@ export function usePageDocument(): PageDocumentHook {
// Build pages by interleaving original pages with insertions
let pages: PDFPage[] = [];
// Helper function to create pages from a file
const createPagesFromFile = (fileId: FileId, startPageNumber: number): PDFPage[] => {
// Helper function to create pages from a file (or placeholder if deselected)
const createPagesFromFile = (fileId: FileId, startPageNumber: number, isSelected: boolean): PDFPage[] => {
const stirlingFileStub = selectors.getStirlingFileStub(fileId);
if (!stirlingFileStub) {
return [];
}
// If file is deselected, create a single placeholder page
if (!isSelected) {
return [{
id: `${fileId}-placeholder`,
pageNumber: startPageNumber,
originalPageNumber: 1,
originalFileId: fileId,
rotation: 0,
thumbnail: null,
selected: false,
splitAfter: false,
isPlaceholder: true,
}];
}
const processedFile = stirlingFileStub.processedFile;
let filePages: PDFPage[] = [];
@ -105,6 +122,7 @@ export function usePageDocument(): PageDocumentHook {
splitAfter: page.splitAfter || false,
originalPageNumber: page.originalPageNumber || page.pageNumber || pageIndex + 1,
originalFileId: fileId,
isPlaceholder: false,
}));
} else if (processedFile?.totalPages) {
// Fallback: create pages without thumbnails but with correct count
@ -117,6 +135,7 @@ export function usePageDocument(): PageDocumentHook {
thumbnail: null,
selected: false,
splitAfter: false,
isPlaceholder: false,
}));
}
@ -124,9 +143,11 @@ export function usePageDocument(): PageDocumentHook {
};
// Collect all pages from original files (without renumbering yet)
const selectedFileIdsSet = new Set(state.ui.selectedFileIds);
const originalFilePages: PDFPage[] = [];
originalFileIds.forEach(fileId => {
const filePages = createPagesFromFile(fileId, 1); // Temporary numbering
const isSelected = selectedFileIdsSet.has(fileId);
const filePages = createPagesFromFile(fileId, 1, isSelected); // Temporary numbering
originalFilePages.push(...filePages);
});
@ -145,7 +166,8 @@ export function usePageDocument(): PageDocumentHook {
// Collect all pages to insert
const allNewPages: PDFPage[] = [];
fileIds.forEach(fileId => {
const insertedPages = createPagesFromFile(fileId, 1);
const isSelected = selectedFileIdsSet.has(fileId);
const insertedPages = createPagesFromFile(fileId, 1, isSelected);
allNewPages.push(...insertedPages);
});

View File

@ -10,6 +10,7 @@ import { FileDropdownMenu } from './FileDropdownMenu';
import { PageEditorFileDropdown } from './PageEditorFileDropdown';
import { usePageEditor } from '../../contexts/PageEditorContext';
import { useFileState } from '../../contexts/FileContext';
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
import { FileId } from '../../types/file';
// Local interface for PageEditor file display
@ -262,10 +263,24 @@ const TopControls = ({
return fileColorAssignments.current;
}, [fileIdsString]);
// Memoize the reorder handler - now much simpler!
// Get pageEditorFunctions from ToolWorkflowContext
const { pageEditorFunctions } = useToolWorkflow();
// Memoize the reorder handler
const handleReorder = useCallback((fromIndex: number, toIndex: number) => {
// Reorder files in PageEditorContext (updates fileOrder)
pageEditorReorderFiles(fromIndex, toIndex);
}, [pageEditorReorderFiles]);
// 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]);
const handleViewChange = useCallback((view: string) => {
if (!isValidWorkbench(view)) {

View File

@ -155,16 +155,47 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) {
React.useEffect(() => {
const currentFileIds = state.files.ids;
// Add new files to the end
// Identify new files
const newFileIds = currentFileIds.filter(id => !fileOrder.includes(id));
// Remove deleted files
const validFileOrder = fileOrder.filter(id => currentFileIds.includes(id));
if (newFileIds.length > 0 || validFileOrder.length !== fileOrder.length) {
setFileOrder([...validFileOrder, ...newFileIds]);
// Check if new files have insertion positions
let hasInsertionPosition = false;
for (const fileId of newFileIds) {
const stub = state.files.byId[fileId];
if (stub?.insertAfterPageId) {
hasInsertionPosition = true;
break;
}
}
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, fileOrder]);
}, [state.files.ids, state.files.byId, fileOrder]);
const updateCurrentPages = useCallback((pages: PDFPage[] | null) => {
setCurrentPages(pages);

View File

@ -75,20 +75,73 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
const { stirlingFileStubs } = action.payload;
const newIds: FileId[] = [];
const newById: Record<FileId, StirlingFileStub> = { ...state.files.byId };
let hasInsertionPosition = false;
let insertAfterPageId: string | undefined;
stirlingFileStubs.forEach(record => {
// Only add if not already present (dedupe by stable ID)
if (!newById[record.id]) {
newIds.push(record.id);
newById[record.id] = record;
// Track if any file has an insertion position
if (record.insertAfterPageId) {
hasInsertionPosition = true;
insertAfterPageId = record.insertAfterPageId;
}
}
});
// 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];
}
// Auto-select inserted files
const newSelectedFileIds = hasInsertionPosition
? [...state.ui.selectedFileIds, ...newIds]
: state.ui.selectedFileIds;
return {
...state,
files: {
ids: [...state.files.ids, ...newIds],
ids: finalIds,
byId: newById
},
ui: {
...state.ui,
selectedFileIds: newSelectedFileIds
}
};
}

View File

@ -1,4 +1,5 @@
import { FileId } from './file';
import { PageBreakSettings } from '../components/pageEditor/commands/pageCommands';
export interface PDFPage {
id: string;
@ -9,7 +10,9 @@ export interface PDFPage {
selected: boolean;
splitAfter?: boolean;
isBlankPage?: boolean;
isPlaceholder?: boolean;
originalFileId?: FileId;
pageBreakSettings?: PageBreakSettings;
}
export interface PDFDocument {
@ -62,6 +65,7 @@ export interface PageEditorFunctions {
onExportSelected: () => void;
onExportAll: () => void;
applyChanges: () => void;
reorderPagesByFileOrder: (newFileOrder: FileId[]) => void;
exportLoading: boolean;
selectionMode: boolean;
selectedPageIds: string[];