diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx
index 79bac508a..2f02315ce 100644
--- a/frontend/src/components/pageEditor/PageEditor.tsx
+++ b/frontend/src/components/pageEditor/PageEditor.tsx
@@ -4,6 +4,7 @@ import { useFileState, useFileActions } from "../../contexts/FileContext";
import { useNavigationGuard } from "../../contexts/NavigationContext";
import { usePageEditor } from "../../contexts/PageEditorContext";
import { PDFDocument, PDFPage, PageEditorFunctions } from "../../types/pageEditor";
+import { StirlingFileStub } from "../../types/fileContext";
import { pdfExportService } from "../../services/pdfExportService";
import { documentManipulationService } from "../../services/documentManipulationService";
import { exportProcessedDocumentsToFiles } from "../../services/pdfExportHelpers";
@@ -30,7 +31,6 @@ import { usePageDocument } from './hooks/usePageDocument';
import { usePageEditorState } from './hooks/usePageEditorState';
import { parseSelection } from "../../utils/bulkselection/parseSelection";
import { usePageEditorRightRailButtons } from "./pageEditorRightRailButtons";
-import { PageBreakSettingsModal } from './PageBreakSettingsModal';
export interface PageEditorProps {
onFunctionsReady?: (functions: PageEditorFunctions) => void;
@@ -167,10 +167,6 @@ 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());
@@ -462,22 +458,13 @@ 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,
- settings
+ setEditedDocument
);
executeCommandWithTracking(pageBreakCommand);
}, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
@@ -496,14 +483,30 @@ const PageEditor = ({
executeCommandWithTracking(pageBreakCommand);
}, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
- const handleInsertFiles = useCallback(async (files: File[], insertAfterPage: number) => {
+ const handleInsertFiles = useCallback(async (
+ files: File[] | StirlingFileStub[],
+ insertAfterPage: number,
+ isFromStorage?: boolean
+ ) => {
if (!displayDocument || files.length === 0) return;
try {
const targetPage = displayDocument.pages.find(p => p.pageNumber === insertAfterPage);
if (!targetPage) return;
- await actions.addFiles(files, { insertAfterPageId: targetPage.id });
+ if (isFromStorage) {
+ // Files from storage - use addStirlingFileStubs to avoid re-storing
+ await actions.addStirlingFileStubs(
+ files as StirlingFileStub[],
+ { insertAfterPageId: targetPage.id, selectFiles: true }
+ );
+ } else {
+ // New uploaded files - use addFiles
+ await actions.addFiles(
+ files as File[],
+ { insertAfterPageId: targetPage.id, selectFiles: true }
+ );
+ }
} catch (error) {
console.error('Failed to insert files:', error);
}
@@ -533,14 +536,9 @@ const PageEditor = ({
const handleReorderPages = useCallback((sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => {
if (!displayDocument) return;
- console.log('=== HANDLE REORDER PAGES ===');
- console.log('selectedPageIds:', selectedPageIds);
-
// Convert selectedPageIds to page numbers for the reorder command
const selectedPages = selectedPageIds ? getPageNumbersFromIds(selectedPageIds) : undefined;
- console.log('selectedPages (converted to numbers):', selectedPages);
-
const reorderCommand = new ReorderPagesCommand(
sourcePageNumber,
targetIndex,
@@ -994,7 +992,7 @@ const PageEditor = ({
const gridOffset = Math.max(0, (containerWidth - gridWidth) / 2);
const leftPosition = gridOffset + col * itemWithGap + ITEM_WIDTH + (ITEM_GAP / 2);
- const topPosition = row * ITEM_HEIGHT + (ITEM_HEIGHT * 0.05); // Center vertically (5% offset since page is 90% height)
+ const topPosition = row * ITEM_HEIGHT + (ITEM_HEIGHT * 0.05) + ITEM_GAP; // Center vertically (5% offset since page is 90% height) + gap offset
return (
- setPageBreakModalOpened(false)}
- onConfirm={handlePageBreakConfirm}
- selectedPageCount={pageBreakModalPageCount}
- />
);
};
diff --git a/frontend/src/components/pageEditor/PageThumbnail.tsx b/frontend/src/components/pageEditor/PageThumbnail.tsx
index 1b51f225c..080a60cf1 100644
--- a/frontend/src/components/pageEditor/PageThumbnail.tsx
+++ b/frontend/src/components/pageEditor/PageThumbnail.tsx
@@ -14,6 +14,7 @@ import { useFilesModalContext } from '../../contexts/FilesModalContext';
import { getFileColorWithOpacity } from './fileColors';
import styles from './PageEditor.module.css';
import HoverActionMenu, { HoverAction } from '../shared/HoverActionMenu';
+import { StirlingFileStub } from '../../types/fileContext';
interface PageThumbnailProps {
@@ -47,7 +48,7 @@ interface PageThumbnailProps {
pdfDocument: PDFDocument;
setPdfDocument: (doc: PDFDocument) => void;
splitPositions: Set;
- onInsertFiles?: (files: File[], insertAfterPage: number) => void;
+ onInsertFiles?: (files: File[] | StirlingFileStub[], insertAfterPage: number, isFromStorage?: boolean) => void;
zoomLevel?: number;
}
@@ -210,9 +211,9 @@ const PageThumbnail: React.FC = ({
// Open file manager modal with custom handler for page insertion
openFilesModal({
insertAfterPage: page.pageNumber,
- customHandler: (files: File[], insertAfterPage?: number) => {
+ customHandler: (files: File[] | StirlingFileStub[], insertAfterPage?: number, isFromStorage?: boolean) => {
if (insertAfterPage !== undefined) {
- onInsertFiles(files, insertAfterPage);
+ onInsertFiles(files, insertAfterPage, isFromStorage);
}
}
});
diff --git a/frontend/src/components/shared/PageEditorFileDropdown.tsx b/frontend/src/components/shared/PageEditorFileDropdown.tsx
index 8038991c5..7eb4b7f72 100644
--- a/frontend/src/components/shared/PageEditorFileDropdown.tsx
+++ b/frontend/src/components/shared/PageEditorFileDropdown.tsx
@@ -36,13 +36,16 @@ const FileMenuItem: React.FC = ({
}) => {
const [isDragging, setIsDragging] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
+ const [dropPosition, setDropPosition] = useState<'above' | 'below'>('below');
const itemRef = useRef(null);
// Keep latest values without re-registering DnD
const indexRef = useRef(index);
const fileIdRef = useRef(file.fileId);
+ const dropPositionRef = useRef<'above' | 'below'>('below');
useEffect(() => { indexRef.current = index; }, [index]);
useEffect(() => { fileIdRef.current = file.fileId; }, [file.fileId]);
+ useEffect(() => { dropPositionRef.current = dropPosition; }, [dropPosition]);
// NEW: keep latest onReorder without effect re-run
const onReorderRef = useRef(onReorder);
@@ -92,15 +95,46 @@ const FileMenuItem: React.FC = ({
toIndex: indexRef.current,
}),
onDragEnter: () => setIsDragOver((p) => (p ? p : true)),
- onDragLeave: () => setIsDragOver((p) => (p ? false : p)),
+ onDragLeave: () => {
+ setIsDragOver((p) => (p ? false : p));
+ setDropPosition('below');
+ },
+ onDrag: ({ source, self }) => {
+ // Determine drop position based on cursor location
+ const element = itemRef.current;
+ if (!element) return;
+
+ const rect = element.getBoundingClientRect();
+ const clientY = (source as any).element?.getBoundingClientRect().top || 0;
+ const midpoint = rect.top + rect.height / 2;
+
+ setDropPosition(clientY < midpoint ? 'below' : 'above');
+ },
onDrop: ({ source }) => {
setIsDragOver(false);
+ const dropPos = dropPositionRef.current;
+ setDropPosition('below');
const sourceData = source.data as any;
if (sourceData?.type === 'file-item') {
const fromIndex = sourceData.fromIndex as number;
- const toIndex = indexRef.current;
+ let toIndex = indexRef.current;
+
+ // Adjust toIndex based on drop position
+ // If dropping below and dragging from above, or dropping above and dragging from below
+ if (dropPos === 'below' && fromIndex < toIndex) {
+ // Dragging down, drop after target - no adjustment needed
+ } else if (dropPos === 'above' && fromIndex > toIndex) {
+ // Dragging up, drop before target - no adjustment needed
+ } else if (dropPos === 'below' && fromIndex > toIndex) {
+ // Dragging up but want below target
+ toIndex = toIndex + 1;
+ } else if (dropPos === 'above' && fromIndex < toIndex) {
+ // Dragging down but want above target
+ toIndex = toIndex - 1;
+ }
+
if (fromIndex !== toIndex) {
- onReorderRef.current(fromIndex, toIndex); // use ref, no re-register
+ onReorderRef.current(fromIndex, toIndex);
}
}
}
@@ -118,40 +152,58 @@ const FileMenuItem: React.FC = ({
return (
{
- e.stopPropagation();
- if (movedRef.current) return; // ignore click after drag
- onToggleSelection(file.fileId);
- }}
style={{
- padding: '0.75rem 0.75rem',
+ position: 'relative',
marginBottom: '0.5rem',
- cursor: isDragging ? 'grabbing' : 'grab',
- backgroundColor: isDragOver ? 'rgba(59, 130, 246, 0.15)' : (file.isSelected ? 'rgba(0, 0, 0, 0.05)' : 'transparent'),
- borderLeft: `6px solid ${fileColorBorder}`,
- borderTop: isDragOver ? '3px solid rgb(59, 130, 246)' : 'none',
- borderBottom: isDragOver ? '3px solid rgb(59, 130, 246)' : 'none',
- opacity: isDragging ? 0.5 : 1,
- transition: 'opacity 0.2s ease-in-out, background-color 0.15s ease, border 0.15s ease',
- userSelect: 'none',
- }}
- onMouseEnter={(e) => {
- if (!isDragging) {
- (e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(0, 0, 0, 0.05)';
- (e.currentTarget as HTMLDivElement).style.borderLeftColor = fileColorBorderHover;
- }
- }}
- onMouseLeave={(e) => {
- if (!isDragging) {
- (e.currentTarget as HTMLDivElement).style.backgroundColor = file.isSelected ? 'rgba(0, 0, 0, 0.05)' : 'transparent';
- (e.currentTarget as HTMLDivElement).style.borderLeftColor = fileColorBorder;
- }
}}
>
+ {/* Drop indicator line */}
+ {isDragOver && (
+
+ )}
+
{
+ e.stopPropagation();
+ if (movedRef.current) return; // ignore click after drag
+ onToggleSelection(file.fileId);
+ }}
+ style={{
+ padding: '0.75rem 0.75rem',
+ cursor: isDragging ? 'grabbing' : 'grab',
+ backgroundColor: file.isSelected ? 'rgba(0, 0, 0, 0.05)' : 'transparent',
+ borderLeft: `6px solid ${fileColorBorder}`,
+ opacity: isDragging ? 0.5 : 1,
+ transition: 'opacity 0.2s ease-in-out, background-color 0.15s ease',
+ userSelect: 'none',
+ }}
+ onMouseEnter={(e) => {
+ if (!isDragging) {
+ (e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(0, 0, 0, 0.05)';
+ (e.currentTarget as HTMLDivElement).style.borderLeftColor = fileColorBorderHover;
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (!isDragging) {
+ (e.currentTarget as HTMLDivElement).style.backgroundColor = file.isSelected ? 'rgba(0, 0, 0, 0.05)' : 'transparent';
+ (e.currentTarget as HTMLDivElement).style.borderLeftColor = fileColorBorder;
+ }
+ }}
+ >
= ({
)}
+
);
};
diff --git a/frontend/src/components/shared/Tooltip.tsx b/frontend/src/components/shared/Tooltip.tsx
index 33491fb44..134af5ed0 100644
--- a/frontend/src/components/shared/Tooltip.tsx
+++ b/frontend/src/components/shared/Tooltip.tsx
@@ -172,7 +172,7 @@ export const Tooltip: React.FC
= ({
const related = e.relatedTarget as Node | null;
// Moving into the tooltip → keep open
- if (related && tooltipRef.current && tooltipRef.current.contains(related)) {
+ if (related && related instanceof Node && tooltipRef.current && tooltipRef.current.contains(related)) {
(children.props as any)?.onPointerLeave?.(e);
return;
}
diff --git a/frontend/src/components/tools/overlayPdfs/OverlayPdfsSettings.tsx b/frontend/src/components/tools/overlayPdfs/OverlayPdfsSettings.tsx
index 43ba64db5..1bb70c924 100644
--- a/frontend/src/components/tools/overlayPdfs/OverlayPdfsSettings.tsx
+++ b/frontend/src/components/tools/overlayPdfs/OverlayPdfsSettings.tsx
@@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next';
import { type OverlayPdfsParameters, type OverlayMode } from '../../../hooks/tools/overlayPdfs/useOverlayPdfsParameters';
import LocalIcon from '../../shared/LocalIcon';
import { useFilesModalContext } from '../../../contexts/FilesModalContext';
+import { StirlingFileStub } from '../../../types/fileContext';
+import { fileStorage } from '../../../services/fileStorage';
import styles from './OverlayPdfsSettings.module.css';
interface OverlayPdfsSettingsProps {
@@ -36,8 +38,22 @@ export default function OverlayPdfsSettings({ parameters, onParameterChange, dis
const handleOpenOverlayFilesModal = () => {
if (disabled) return;
openFilesModal({
- customHandler: (files: File[]) => {
- handleOverlayFilesChange([...(parameters.overlayFiles || []), ...files]);
+ customHandler: async (files: File[] | StirlingFileStub[], _insertAfterPage?: number, isFromStorage?: boolean) => {
+ let resolvedFiles: File[] = [];
+
+ if (isFromStorage) {
+ // Load actual File objects from storage
+ for (const stub of files as StirlingFileStub[]) {
+ const stirlingFile = await fileStorage.getStirlingFile(stub.id);
+ if (stirlingFile) {
+ resolvedFiles.push(stirlingFile);
+ }
+ }
+ } else {
+ resolvedFiles = files as File[];
+ }
+
+ handleOverlayFilesChange([...(parameters.overlayFiles || []), ...resolvedFiles]);
}
});
};
diff --git a/frontend/src/contexts/FilesModalContext.tsx b/frontend/src/contexts/FilesModalContext.tsx
index e850fa4dd..88bd26e3f 100644
--- a/frontend/src/contexts/FilesModalContext.tsx
+++ b/frontend/src/contexts/FilesModalContext.tsx
@@ -6,7 +6,7 @@ import { fileStorage } from '../services/fileStorage';
interface FilesModalContextType {
isFilesModalOpen: boolean;
- openFilesModal: (options?: { insertAfterPage?: number; customHandler?: (files: File[], insertAfterPage?: number) => void }) => void;
+ openFilesModal: (options?: { insertAfterPage?: number; customHandler?: (files: File[] | StirlingFileStub[], insertAfterPage?: number, isFromStorage?: boolean) => void }) => void;
closeFilesModal: () => void;
onFileUpload: (files: File[]) => void;
onRecentFileSelect: (stirlingFileStubs: StirlingFileStub[]) => void;
@@ -22,9 +22,9 @@ export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ ch
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
const [onModalClose, setOnModalClose] = useState<(() => void) | undefined>();
const [insertAfterPage, setInsertAfterPage] = useState();
- const [customHandler, setCustomHandler] = useState<((files: File[], insertAfterPage?: number) => void) | undefined>();
+ const [customHandler, setCustomHandler] = useState<((files: File[] | StirlingFileStub[], insertAfterPage?: number, isFromStorage?: boolean) => void) | undefined>();
- const openFilesModal = useCallback((options?: { insertAfterPage?: number; customHandler?: (files: File[], insertAfterPage?: number) => void }) => {
+ const openFilesModal = useCallback((options?: { insertAfterPage?: number; customHandler?: (files: File[] | StirlingFileStub[], insertAfterPage?: number, isFromStorage?: boolean) => void }) => {
setInsertAfterPage(options?.insertAfterPage);
setCustomHandler(() => options?.customHandler);
setIsFilesModalOpen(true);
@@ -50,22 +50,8 @@ export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ ch
const handleRecentFileSelect = useCallback(async (stirlingFileStubs: StirlingFileStub[]) => {
if (customHandler) {
- // Load the actual files from storage for custom handler
- try {
- const loadedFiles: File[] = [];
- for (const stub of stirlingFileStubs) {
- const stirlingFile = await fileStorage.getStirlingFile(stub.id);
- if (stirlingFile) {
- loadedFiles.push(stirlingFile);
- }
- }
-
- if (loadedFiles.length > 0) {
- customHandler(loadedFiles, insertAfterPage);
- }
- } catch (error) {
- console.error('Failed to load files for custom handler:', error);
- }
+ // Pass stubs directly to custom handler with flag indicating they're from storage
+ customHandler(stirlingFileStubs, insertAfterPage, true);
} else {
// Normal case - use addStirlingFileStubs to preserve metadata
if (actions.addStirlingFileStubs) {
diff --git a/frontend/src/contexts/file/FileReducer.ts b/frontend/src/contexts/file/FileReducer.ts
index 5fcd26b48..0a01c4ec4 100644
--- a/frontend/src/contexts/file/FileReducer.ts
+++ b/frontend/src/contexts/file/FileReducer.ts
@@ -82,13 +82,16 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
// 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;
}
+
+ // Store record but clear insertAfterPageId (it's only used once)
+ const { insertAfterPageId: _, ...recordWithoutInsertPosition } = record;
+ newById[record.id] = recordWithoutInsertPosition;
}
});