mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-01 20:10:35 +01:00
Rejig arrays
This commit is contained in:
parent
05a7161412
commit
e7c6db082c
@ -6,7 +6,7 @@ import { useFileHandler } from '../../hooks/useFileHandler';
|
|||||||
import { useFileState } from '../../contexts/FileContext';
|
import { useFileState } from '../../contexts/FileContext';
|
||||||
import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext';
|
import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext';
|
||||||
import { useViewer } from '../../contexts/ViewerContext';
|
import { useViewer } from '../../contexts/ViewerContext';
|
||||||
import { PageEditorProvider, usePageEditor } from '../../contexts/PageEditorContext';
|
import { PageEditorProvider } from '../../contexts/PageEditorContext';
|
||||||
import './Workbench.css';
|
import './Workbench.css';
|
||||||
|
|
||||||
import TopControls from '../shared/TopControls';
|
import TopControls from '../shared/TopControls';
|
||||||
@ -18,33 +18,6 @@ import LandingPage from '../shared/LandingPage';
|
|||||||
import Footer from '../shared/Footer';
|
import Footer from '../shared/Footer';
|
||||||
import DismissAllErrorsButton from '../shared/DismissAllErrorsButton';
|
import DismissAllErrorsButton from '../shared/DismissAllErrorsButton';
|
||||||
|
|
||||||
// Syncs PageEditorContext with FileContext on activeFiles change
|
|
||||||
function PageEditorSync({ activeFiles }: { activeFiles: Array<{ fileId: any; name: string; versionNumber?: number }> }) {
|
|
||||||
const { syncWithFileContext } = usePageEditor();
|
|
||||||
|
|
||||||
// Create stable signature to prevent unnecessary syncs
|
|
||||||
const fileSignature = useMemo(
|
|
||||||
() => activeFiles.map(f => `${f.fileId}-${f.name}-${f.versionNumber || 0}`).join('|'),
|
|
||||||
[activeFiles]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Filter for PDFs only - Page Editor doesn't support images
|
|
||||||
const pdfFiles = activeFiles.filter(f =>
|
|
||||||
f.name.toLowerCase().endsWith('.pdf')
|
|
||||||
);
|
|
||||||
|
|
||||||
const fileData = pdfFiles.map(f => ({
|
|
||||||
fileId: f.fileId,
|
|
||||||
name: f.name,
|
|
||||||
versionNumber: f.versionNumber,
|
|
||||||
}));
|
|
||||||
syncWithFileContext(fileData);
|
|
||||||
}, [fileSignature, syncWithFileContext, activeFiles]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No props needed - component uses contexts directly
|
// No props needed - component uses contexts directly
|
||||||
export default function Workbench() {
|
export default function Workbench() {
|
||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
@ -79,9 +52,6 @@ export default function Workbench() {
|
|||||||
// Get active file index from ViewerContext
|
// Get active file index from ViewerContext
|
||||||
const { activeFileIndex, setActiveFileIndex } = useViewer();
|
const { activeFileIndex, setActiveFileIndex } = useViewer();
|
||||||
|
|
||||||
// Get all file IDs for PageEditor initialization
|
|
||||||
const allFileIds = useMemo(() => activeFiles.map(f => f.fileId), [activeFiles]);
|
|
||||||
|
|
||||||
const handlePreviewClose = () => {
|
const handlePreviewClose = () => {
|
||||||
setPreviewFile(null);
|
setPreviewFile(null);
|
||||||
const previousMode = sessionStorage.getItem('previousMode');
|
const previousMode = sessionStorage.getItem('previousMode');
|
||||||
@ -177,8 +147,7 @@ export default function Workbench() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageEditorProvider initialFileIds={allFileIds}>
|
<PageEditorProvider>
|
||||||
<PageEditorSync activeFiles={activeFiles} />
|
|
||||||
<Box
|
<Box
|
||||||
className="flex-1 h-full min-w-80 relative flex flex-col"
|
className="flex-1 h-full min-w-80 relative flex flex-col"
|
||||||
style={
|
style={
|
||||||
|
|||||||
@ -43,8 +43,64 @@ const PageEditor = ({
|
|||||||
// Navigation guard for unsaved changes
|
// Navigation guard for unsaved changes
|
||||||
const { setHasUnsavedChanges } = useNavigationGuard();
|
const { setHasUnsavedChanges } = useNavigationGuard();
|
||||||
|
|
||||||
// Get files from PageEditorContext (synced by Workbench)
|
// Get PageEditor coordination functions
|
||||||
const { files: pageEditorFiles, updateCurrentPages, reorderedPages, clearReorderedPages, updateFileOrderFromPages, lastReorderSource, clearReorderSource } = usePageEditor();
|
const { updateCurrentPages, reorderedPages, clearReorderedPages, updateFileOrderFromPages } = usePageEditor();
|
||||||
|
|
||||||
|
// Derive page editor files from FileContext (single source of truth)
|
||||||
|
// 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 selectedIdsKey = [...state.ui.selectedFileIds].sort().join(',');
|
||||||
|
const filesSignature = selectors.getFilesSignature();
|
||||||
|
|
||||||
|
const fileObjectsRef = useRef(new Map<FileId, any>());
|
||||||
|
|
||||||
|
const pageEditorFiles = useMemo(() => {
|
||||||
|
const cache = fileObjectsRef.current;
|
||||||
|
const newFiles: any[] = [];
|
||||||
|
|
||||||
|
state.files.ids.forEach(fileId => {
|
||||||
|
const stub = selectors.getStirlingFileStub(fileId);
|
||||||
|
const isSelected = state.ui.selectedFileIds.includes(fileId);
|
||||||
|
const isPdf = stub?.name?.toLowerCase().endsWith('.pdf') ?? false;
|
||||||
|
|
||||||
|
if (!isPdf) return; // Skip non-PDFs
|
||||||
|
|
||||||
|
const cached = cache.get(fileId);
|
||||||
|
|
||||||
|
// Check if data actually changed (compare by fileId, not position)
|
||||||
|
if (cached &&
|
||||||
|
cached.fileId === fileId &&
|
||||||
|
cached.name === (stub?.name || '') &&
|
||||||
|
cached.versionNumber === stub?.versionNumber &&
|
||||||
|
cached.isSelected === isSelected) {
|
||||||
|
// Reuse existing object reference
|
||||||
|
newFiles.push(cached);
|
||||||
|
} else {
|
||||||
|
// Create new object only if data changed
|
||||||
|
const newFile = {
|
||||||
|
fileId,
|
||||||
|
name: stub?.name || '',
|
||||||
|
versionNumber: stub?.versionNumber,
|
||||||
|
isSelected,
|
||||||
|
};
|
||||||
|
cache.set(fileId, newFile);
|
||||||
|
newFiles.push(newFile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up removed files from cache
|
||||||
|
const activeIds = new Set(newFiles.map(f => f.fileId));
|
||||||
|
for (const cachedId of cache.keys()) {
|
||||||
|
if (!activeIds.has(cachedId)) {
|
||||||
|
cache.delete(cachedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newFiles;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [fileIdsKey, selectedIdsKey, filesSignature]);
|
||||||
|
|
||||||
// Get active file IDs from SELECTED files only
|
// Get active file IDs from SELECTED files only
|
||||||
const activeFileIds = useMemo(() => {
|
const activeFileIds = useMemo(() => {
|
||||||
@ -143,45 +199,15 @@ const PageEditor = ({
|
|||||||
};
|
};
|
||||||
setEditedDocument(reorderedDocument);
|
setEditedDocument(reorderedDocument);
|
||||||
clearReorderedPages();
|
clearReorderedPages();
|
||||||
// Clear the source after applying to prevent feedback loop
|
|
||||||
clearReorderSource();
|
|
||||||
}
|
}
|
||||||
}, [reorderedPages, displayDocument, clearReorderedPages, clearReorderSource]);
|
}, [reorderedPages, displayDocument, clearReorderedPages]);
|
||||||
|
|
||||||
// Update file order when pages are manually reordered
|
// Update file order when pages are manually reordered
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Skip if the last reorder came from file-level (prevent feedback loop)
|
|
||||||
if (lastReorderSource === 'file') {
|
|
||||||
clearReorderSource();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editedDocument?.pages && editedDocument.pages.length > 0 && activeFileIds.length > 1) {
|
if (editedDocument?.pages && editedDocument.pages.length > 0 && activeFileIds.length > 1) {
|
||||||
// Compute the file order based on page positions
|
updateFileOrderFromPages(editedDocument.pages);
|
||||||
const fileFirstPagePositions = new Map<FileId, number>();
|
|
||||||
editedDocument.pages.forEach((page, index) => {
|
|
||||||
const fileId = page.originalFileId;
|
|
||||||
if (!fileId) return;
|
|
||||||
if (!fileFirstPagePositions.has(fileId)) {
|
|
||||||
fileFirstPagePositions.set(fileId, index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort files by their first page position
|
|
||||||
const computedFileOrder = Array.from(fileFirstPagePositions.entries())
|
|
||||||
.sort((a, b) => a[1] - b[1])
|
|
||||||
.map(entry => entry[0]);
|
|
||||||
|
|
||||||
// Check if the order has actually changed
|
|
||||||
const currentFileOrder = pageEditorFiles.filter(f => f.isSelected).map(f => f.fileId);
|
|
||||||
const orderChanged = computedFileOrder.length !== currentFileOrder.length ||
|
|
||||||
computedFileOrder.some((id, index) => id !== currentFileOrder[index]);
|
|
||||||
|
|
||||||
if (orderChanged && computedFileOrder.length > 0) {
|
|
||||||
updateFileOrderFromPages(editedDocument.pages);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [editedDocument?.pages, activeFileIds.length, state.files.ids, pageEditorFiles, updateFileOrderFromPages, lastReorderSource, clearReorderSource]);
|
}, [editedDocument?.pages, activeFileIds.length, updateFileOrderFromPages]);
|
||||||
|
|
||||||
// Utility functions to convert between page IDs and page numbers
|
// Utility functions to convert between page IDs and page numbers
|
||||||
const getPageNumbersFromIds = useCallback((pageIds: string[]): number[] => {
|
const getPageNumbersFromIds = useCallback((pageIds: string[]): number[] => {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useFileState } from '../../../contexts/FileContext';
|
import { useFileState } from '../../../contexts/FileContext';
|
||||||
import { usePageEditor } from '../../../contexts/PageEditorContext';
|
|
||||||
import { PDFDocument, PDFPage } from '../../../types/pageEditor';
|
import { PDFDocument, PDFPage } from '../../../types/pageEditor';
|
||||||
import { FileId } from '../../../types/file';
|
import { FileId } from '../../../types/file';
|
||||||
|
|
||||||
@ -16,28 +15,29 @@ export interface PageDocumentHook {
|
|||||||
*/
|
*/
|
||||||
export function usePageDocument(): PageDocumentHook {
|
export function usePageDocument(): PageDocumentHook {
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
const { files: pageEditorFiles } = usePageEditor();
|
|
||||||
|
|
||||||
// Convert Set to array and filter to maintain file order from FileContext
|
// Convert Set to array and filter to maintain file order from FileContext
|
||||||
const allFileIds = state.files.ids;
|
const allFileIds = state.files.ids;
|
||||||
|
|
||||||
// Create stable string representations for useMemo dependencies
|
// Derive selected file IDs directly from FileContext (single source of truth)
|
||||||
const allFileIdsString = allFileIds.join(',');
|
// Filter to only include PDF files (PageEditor only supports PDFs)
|
||||||
const selectedFiles = pageEditorFiles.filter(f => f.isSelected);
|
// Use stable string keys to prevent infinite loops
|
||||||
const selectedIdsString = selectedFiles.map(f => f.fileId).sort().join(',');
|
const allFileIdsKey = allFileIds.join(',');
|
||||||
|
const selectedIdsKey = [...state.ui.selectedFileIds].sort().join(',');
|
||||||
|
const filesSignature = selectors.getFilesSignature();
|
||||||
|
|
||||||
const activeFileIds = useMemo(() => {
|
const activeFileIds = useMemo(() => {
|
||||||
const selectedFileIds = new Set(selectedFiles.map(f => f.fileId));
|
const selectedFileIds = new Set(state.ui.selectedFileIds);
|
||||||
return allFileIds.filter(id => selectedFileIds.has(id));
|
return allFileIds.filter(id => {
|
||||||
// Using string representations to prevent infinite loops
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [allFileIdsString, selectedIdsString]);
|
}, [allFileIdsKey, selectedIdsKey, filesSignature]);
|
||||||
|
|
||||||
const primaryFileId = activeFileIds[0] ?? null;
|
const primaryFileId = activeFileIds[0] ?? null;
|
||||||
|
|
||||||
// Stable signature for effects (prevents loops)
|
|
||||||
const filesSignature = selectors.getFilesSignature();
|
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
const globalProcessing = state.ui.isProcessing;
|
const globalProcessing = state.ui.isProcessing;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useRef, useCallback, useState } from 'react';
|
import React, { useRef, useCallback, useState, useEffect } from 'react';
|
||||||
import { Menu, Loader, Group, Text, Checkbox, ActionIcon } from '@mantine/core';
|
import { Menu, Loader, Group, Text, Checkbox, ActionIcon } from '@mantine/core';
|
||||||
import EditNoteIcon from '@mui/icons-material/EditNote';
|
import EditNoteIcon from '@mui/icons-material/EditNote';
|
||||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||||
@ -12,7 +12,14 @@ import FitText from './FitText';
|
|||||||
import { getFileColorWithOpacity } from '../pageEditor/fileColors';
|
import { getFileColorWithOpacity } from '../pageEditor/fileColors';
|
||||||
|
|
||||||
import { FileId } from '../../types/file';
|
import { FileId } from '../../types/file';
|
||||||
import { PageEditorFile } from '../../contexts/PageEditorContext';
|
|
||||||
|
// Local interface for PageEditor file display
|
||||||
|
interface PageEditorFile {
|
||||||
|
fileId: FileId;
|
||||||
|
name: string;
|
||||||
|
versionNumber?: number;
|
||||||
|
isSelected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface FileMenuItemProps {
|
interface FileMenuItemProps {
|
||||||
file: PageEditorFile;
|
file: PageEditorFile;
|
||||||
@ -45,61 +52,79 @@ const FileMenuItem: React.FC<FileMenuItemProps> = ({
|
|||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const itemRef = useRef<HTMLDivElement>(null);
|
const itemRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const itemElementRef = useCallback((element: HTMLDivElement | null) => {
|
// Keep latest values without re-registering DnD
|
||||||
if (element) {
|
const indexRef = useRef(index);
|
||||||
itemRef.current = element;
|
const fileIdRef = useRef(file.fileId);
|
||||||
|
useEffect(() => { indexRef.current = index; }, [index]);
|
||||||
|
useEffect(() => { fileIdRef.current = file.fileId; }, [file.fileId]);
|
||||||
|
|
||||||
const dragCleanup = draggable({
|
// NEW: keep latest onReorder without effect re-run
|
||||||
element,
|
const onReorderRef = useRef(onReorder);
|
||||||
getInitialData: () => ({
|
useEffect(() => { onReorderRef.current = onReorder; }, [onReorder]);
|
||||||
type: 'file-item',
|
|
||||||
fileId: file.fileId,
|
|
||||||
fromIndex: index,
|
|
||||||
}),
|
|
||||||
onDragStart: () => {
|
|
||||||
setIsDragging(true);
|
|
||||||
},
|
|
||||||
onDrop: () => {
|
|
||||||
setIsDragging(false);
|
|
||||||
},
|
|
||||||
canDrag: () => true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const dropCleanup = dropTargetForElements({
|
// Gesture guard for row click vs drag
|
||||||
element,
|
const movedRef = useRef(false);
|
||||||
getData: () => ({
|
const startRef = useRef<{ x: number; y: number } | null>(null);
|
||||||
type: 'file-item',
|
|
||||||
fileId: file.fileId,
|
const onPointerDown = (e: React.PointerEvent) => {
|
||||||
toIndex: index,
|
startRef.current = { x: e.clientX, y: e.clientY };
|
||||||
}),
|
movedRef.current = false;
|
||||||
onDragEnter: () => {
|
};
|
||||||
setIsDragOver(true);
|
|
||||||
},
|
const onPointerMove = (e: React.PointerEvent) => {
|
||||||
onDragLeave: () => {
|
if (!startRef.current) return;
|
||||||
setIsDragOver(false);
|
const dx = e.clientX - startRef.current.x;
|
||||||
},
|
const dy = e.clientY - startRef.current.y;
|
||||||
onDrop: ({ source }) => {
|
if (dx * dx + dy * dy > 25) movedRef.current = true; // ~5px threshold
|
||||||
setIsDragOver(false);
|
};
|
||||||
const sourceData = source.data;
|
|
||||||
if (sourceData.type === 'file-item') {
|
const onPointerUp = () => {
|
||||||
const fromIndex = sourceData.fromIndex as number;
|
startRef.current = null;
|
||||||
if (fromIndex !== index) {
|
};
|
||||||
onReorder(fromIndex, index);
|
|
||||||
}
|
useEffect(() => {
|
||||||
|
const element = itemRef.current;
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
const dragCleanup = draggable({
|
||||||
|
element,
|
||||||
|
getInitialData: () => ({
|
||||||
|
type: 'file-item',
|
||||||
|
fileId: fileIdRef.current,
|
||||||
|
fromIndex: indexRef.current,
|
||||||
|
}),
|
||||||
|
onDragStart: () => setIsDragging((p) => (p ? p : true)),
|
||||||
|
onDrop: () => setIsDragging((p) => (p ? false : p)),
|
||||||
|
canDrag: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dropCleanup = dropTargetForElements({
|
||||||
|
element,
|
||||||
|
getData: () => ({
|
||||||
|
type: 'file-item',
|
||||||
|
fileId: fileIdRef.current,
|
||||||
|
toIndex: indexRef.current,
|
||||||
|
}),
|
||||||
|
onDragEnter: () => setIsDragOver((p) => (p ? p : true)),
|
||||||
|
onDragLeave: () => setIsDragOver((p) => (p ? false : p)),
|
||||||
|
onDrop: ({ source }) => {
|
||||||
|
setIsDragOver(false);
|
||||||
|
const sourceData = source.data as any;
|
||||||
|
if (sourceData?.type === 'file-item') {
|
||||||
|
const fromIndex = sourceData.fromIndex as number;
|
||||||
|
const toIndex = indexRef.current;
|
||||||
|
if (fromIndex !== toIndex) {
|
||||||
|
onReorderRef.current(fromIndex, toIndex); // use ref, no re-register
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
(element as any).__dragCleanup = () => {
|
|
||||||
dragCleanup();
|
|
||||||
dropCleanup();
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
if (itemRef.current && (itemRef.current as any).__dragCleanup) {
|
|
||||||
(itemRef.current as any).__dragCleanup();
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}, [file.fileId, index, onReorder]);
|
|
||||||
|
return () => {
|
||||||
|
try { dragCleanup(); } catch {}
|
||||||
|
try { dropCleanup(); } catch {}
|
||||||
|
};
|
||||||
|
}, []); // NOTE: no `onReorder` here
|
||||||
|
|
||||||
const itemName = file?.name || 'Untitled';
|
const itemName = file?.name || 'Untitled';
|
||||||
const fileColorBorder = getFileColorWithOpacity(colorIndex, 1);
|
const fileColorBorder = getFileColorWithOpacity(colorIndex, 1);
|
||||||
@ -107,9 +132,13 @@ const FileMenuItem: React.FC<FileMenuItemProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={itemElementRef}
|
ref={itemRef}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
if (movedRef.current) return; // ignore click after drag
|
||||||
onToggleSelection(file.fileId);
|
onToggleSelection(file.fileId);
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
import React, { useState, useCallback, useMemo } from "react";
|
import React, { useState, useCallback, useMemo } from "react";
|
||||||
// Component to sync PageEditorContext with FileContext
|
|
||||||
// Must be inside PageEditorProvider to access usePageEditor
|
|
||||||
import { SegmentedControl, Loader } from "@mantine/core";
|
import { SegmentedControl, Loader } from "@mantine/core";
|
||||||
import { useRainbowThemeContext } from "./RainbowThemeProvider";
|
import { useRainbowThemeContext } from "./RainbowThemeProvider";
|
||||||
import rainbowStyles from '../../styles/rainbow.module.css';
|
import rainbowStyles from '../../styles/rainbow.module.css';
|
||||||
@ -11,49 +9,63 @@ import { WorkbenchType, isValidWorkbench } from '../../types/workbench';
|
|||||||
import { FileDropdownMenu } from './FileDropdownMenu';
|
import { FileDropdownMenu } from './FileDropdownMenu';
|
||||||
import { PageEditorFileDropdown } from './PageEditorFileDropdown';
|
import { PageEditorFileDropdown } from './PageEditorFileDropdown';
|
||||||
import { usePageEditor } from '../../contexts/PageEditorContext';
|
import { usePageEditor } from '../../contexts/PageEditorContext';
|
||||||
|
import { useFileState } from '../../contexts/FileContext';
|
||||||
import { FileId } from '../../types/file';
|
import { FileId } from '../../types/file';
|
||||||
|
|
||||||
|
// Local interface for PageEditor file display
|
||||||
|
interface PageEditorFile {
|
||||||
|
fileId: FileId;
|
||||||
|
name: string;
|
||||||
|
versionNumber?: number;
|
||||||
|
isSelected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageEditorState {
|
||||||
|
files: PageEditorFile[];
|
||||||
|
selectedCount: number;
|
||||||
|
totalCount: number;
|
||||||
|
onToggleSelection: (fileId: FileId) => void;
|
||||||
|
onReorder: (fromIndex: number, toIndex: number) => void;
|
||||||
|
fileColorMap: Map<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// View option styling
|
||||||
const viewOptionStyle: React.CSSProperties = {
|
const viewOptionStyle: React.CSSProperties = {
|
||||||
display: 'inline-flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 6,
|
gap: '0.5rem',
|
||||||
whiteSpace: 'nowrap',
|
justifyContent: 'center',
|
||||||
paddingTop: '0.3rem',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to create view options for SegmentedControl
|
||||||
// Build view options showing text always
|
|
||||||
const createViewOptions = (
|
const createViewOptions = (
|
||||||
currentView: WorkbenchType,
|
currentView: WorkbenchType,
|
||||||
switchingTo: WorkbenchType | null,
|
switchingTo: WorkbenchType | null,
|
||||||
activeFiles: Array<{ fileId: string | FileId; name: string; versionNumber?: number }>,
|
activeFiles?: Array<{ fileId: string; name: string; versionNumber?: number }>,
|
||||||
currentFileIndex: number,
|
currentFileIndex?: number,
|
||||||
onFileSelect?: (index: number) => void,
|
onFileSelect?: (index: number) => void,
|
||||||
pageEditorState?: {
|
pageEditorState?: PageEditorState
|
||||||
files: Array<{ fileId: FileId; name: string; versionNumber?: number; isSelected: boolean }>;
|
|
||||||
selectedCount: number;
|
|
||||||
totalCount: number;
|
|
||||||
onToggleSelection: (fileId: FileId) => void;
|
|
||||||
onReorder: (fromIndex: number, toIndex: number) => void;
|
|
||||||
fileColorMap: Map<string, number>;
|
|
||||||
}
|
|
||||||
) => {
|
) => {
|
||||||
const currentFile = activeFiles[currentFileIndex];
|
// Viewer dropdown logic
|
||||||
const isInViewer = currentView === 'viewer';
|
const isInViewer = currentView === 'viewer';
|
||||||
const fileName = currentFile?.name || '';
|
const hasActiveFiles = activeFiles && activeFiles.length > 0;
|
||||||
const displayName = isInViewer && fileName ? fileName : 'Viewer';
|
const showViewerDropdown = isInViewer && hasActiveFiles;
|
||||||
const hasMultipleFiles = activeFiles.length > 1;
|
|
||||||
const showDropdown = isInViewer && hasMultipleFiles;
|
let viewerDisplayName = 'Viewer';
|
||||||
|
if (isInViewer && hasActiveFiles && currentFileIndex !== undefined) {
|
||||||
|
const currentFile = activeFiles[currentFileIndex];
|
||||||
|
if (currentFile) {
|
||||||
|
viewerDisplayName = currentFile.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const viewerOption = {
|
const viewerOption = {
|
||||||
label: showDropdown ? (
|
label: showViewerDropdown ? (
|
||||||
<FileDropdownMenu
|
<FileDropdownMenu
|
||||||
displayName={displayName}
|
displayName={viewerDisplayName}
|
||||||
activeFiles={activeFiles}
|
activeFiles={activeFiles!}
|
||||||
currentFileIndex={currentFileIndex}
|
currentFileIndex={currentFileIndex!}
|
||||||
onFileSelect={onFileSelect}
|
onFileSelect={onFileSelect!}
|
||||||
switchingTo={switchingTo}
|
switchingTo={switchingTo}
|
||||||
viewOptionStyle={viewOptionStyle}
|
viewOptionStyle={viewOptionStyle}
|
||||||
/>
|
/>
|
||||||
@ -64,7 +76,7 @@ const createViewOptions = (
|
|||||||
) : (
|
) : (
|
||||||
<VisibilityIcon fontSize="small" />
|
<VisibilityIcon fontSize="small" />
|
||||||
)}
|
)}
|
||||||
<span>{displayName}</span>
|
<span>{viewerDisplayName}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
value: "viewer",
|
value: "viewer",
|
||||||
@ -149,12 +161,71 @@ const TopControls = ({
|
|||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
|
const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
|
||||||
|
|
||||||
// Get page editor state for dropdown
|
// Get FileContext state and PageEditor coordination functions
|
||||||
|
const { state, selectors } = useFileState();
|
||||||
|
const pageEditorContext = usePageEditor();
|
||||||
const {
|
const {
|
||||||
files: pageEditorFiles = [],
|
|
||||||
toggleFileSelection,
|
toggleFileSelection,
|
||||||
reorderFiles: pageEditorReorderFiles,
|
reorderFiles: pageEditorReorderFiles,
|
||||||
} = usePageEditor();
|
fileOrder: pageEditorFileOrder,
|
||||||
|
} = pageEditorContext;
|
||||||
|
|
||||||
|
// Derive page editor files from PageEditorContext.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 fileOrderKey = pageEditorFileOrder.join(',');
|
||||||
|
const selectedIdsKey = [...state.ui.selectedFileIds].sort().join(',');
|
||||||
|
const filesSignature = selectors.getFilesSignature();
|
||||||
|
|
||||||
|
const fileObjectsRef = React.useRef(new Map<FileId, PageEditorFile>());
|
||||||
|
|
||||||
|
const pageEditorFiles = useMemo<PageEditorFile[]>(() => {
|
||||||
|
const cache = fileObjectsRef.current;
|
||||||
|
const newFiles: PageEditorFile[] = [];
|
||||||
|
|
||||||
|
// Use PageEditorContext.fileOrder instead of state.files.ids
|
||||||
|
pageEditorFileOrder.forEach(fileId => {
|
||||||
|
const stub = selectors.getStirlingFileStub(fileId);
|
||||||
|
const isSelected = state.ui.selectedFileIds.includes(fileId);
|
||||||
|
const isPdf = stub?.name?.toLowerCase().endsWith('.pdf') ?? false;
|
||||||
|
|
||||||
|
if (!isPdf) return; // Skip non-PDFs
|
||||||
|
|
||||||
|
const cached = cache.get(fileId);
|
||||||
|
|
||||||
|
// Check if data actually changed (compare by fileId, not position)
|
||||||
|
if (cached &&
|
||||||
|
cached.fileId === fileId &&
|
||||||
|
cached.name === (stub?.name || '') &&
|
||||||
|
cached.versionNumber === stub?.versionNumber &&
|
||||||
|
cached.isSelected === isSelected) {
|
||||||
|
// Reuse existing object reference
|
||||||
|
newFiles.push(cached);
|
||||||
|
} else {
|
||||||
|
// Create new object only if data changed
|
||||||
|
const newFile: PageEditorFile = {
|
||||||
|
fileId,
|
||||||
|
name: stub?.name || '',
|
||||||
|
versionNumber: stub?.versionNumber,
|
||||||
|
isSelected,
|
||||||
|
};
|
||||||
|
cache.set(fileId, newFile);
|
||||||
|
newFiles.push(newFile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up removed files from cache
|
||||||
|
const activeIds = new Set(newFiles.map(f => f.fileId));
|
||||||
|
for (const cachedId of cache.keys()) {
|
||||||
|
if (!activeIds.has(cachedId)) {
|
||||||
|
cache.delete(cachedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newFiles;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [fileOrderKey, selectedIdsKey, filesSignature, pageEditorFileOrder, state.ui.selectedFileIds, selectors]);
|
||||||
|
|
||||||
// Convert to counts
|
// Convert to counts
|
||||||
const selectedCount = pageEditorFiles?.filter(f => f.isSelected).length || 0;
|
const selectedCount = pageEditorFiles?.filter(f => f.isSelected).length || 0;
|
||||||
|
|||||||
@ -1,15 +1,11 @@
|
|||||||
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
|
import React, { createContext, useContext, useState, useCallback, ReactNode, useMemo } from 'react';
|
||||||
import { FileId } from '../types/file';
|
import { FileId } from '../types/file';
|
||||||
import { useFileActions } from './FileContext';
|
import { useFileActions, useFileState } from './FileContext';
|
||||||
import { PDFPage } from '../types/pageEditor';
|
import { PDFPage } from '../types/pageEditor';
|
||||||
import { MAX_PAGE_EDITOR_FILES } from '../components/pageEditor/fileColors';
|
import { MAX_PAGE_EDITOR_FILES } from '../components/pageEditor/fileColors';
|
||||||
|
|
||||||
export interface PageEditorFile {
|
// PageEditorFile is now defined locally in consuming components
|
||||||
fileId: FileId;
|
// Components should derive file list directly from FileContext
|
||||||
name: string;
|
|
||||||
versionNumber?: number;
|
|
||||||
isSelected: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes file order based on the position of each file's first page
|
* Computes file order based on the position of each file's first page
|
||||||
@ -103,9 +99,6 @@ function reorderPagesForFileMove(
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface PageEditorContextValue {
|
interface PageEditorContextValue {
|
||||||
// Single array of files with selection state
|
|
||||||
files: PageEditorFile[];
|
|
||||||
|
|
||||||
// Current page order (updated by PageEditor, used for file reordering)
|
// Current page order (updated by PageEditor, used for file reordering)
|
||||||
currentPages: PDFPage[] | null;
|
currentPages: PDFPage[] | null;
|
||||||
updateCurrentPages: (pages: PDFPage[] | null) => void;
|
updateCurrentPages: (pages: PDFPage[] | null) => void;
|
||||||
@ -114,45 +107,64 @@ interface PageEditorContextValue {
|
|||||||
reorderedPages: PDFPage[] | null;
|
reorderedPages: PDFPage[] | null;
|
||||||
clearReorderedPages: () => void;
|
clearReorderedPages: () => void;
|
||||||
|
|
||||||
// Set file selection
|
// Page editor's own file order (independent of FileContext global order)
|
||||||
|
fileOrder: FileId[];
|
||||||
|
setFileOrder: (order: FileId[]) => void;
|
||||||
|
|
||||||
|
// Set file selection (calls FileContext actions)
|
||||||
setFileSelection: (fileId: FileId, selected: boolean) => void;
|
setFileSelection: (fileId: FileId, selected: boolean) => void;
|
||||||
|
|
||||||
// Toggle file selection
|
// Toggle file selection (calls FileContext actions)
|
||||||
toggleFileSelection: (fileId: FileId) => void;
|
toggleFileSelection: (fileId: FileId) => void;
|
||||||
|
|
||||||
// Select/deselect all files
|
// Select/deselect all files (calls FileContext actions)
|
||||||
selectAll: () => void;
|
selectAll: () => void;
|
||||||
deselectAll: () => void;
|
deselectAll: () => void;
|
||||||
|
|
||||||
// Reorder files (simple array reordering)
|
// Reorder files (only affects page editor's local order)
|
||||||
reorderFiles: (fromIndex: number, toIndex: number) => void;
|
reorderFiles: (fromIndex: number, toIndex: number) => void;
|
||||||
|
|
||||||
// Update file order based on page positions (when pages are manually reordered)
|
// Update file order based on page positions (when pages are manually reordered)
|
||||||
updateFileOrderFromPages: (pages: PDFPage[]) => void;
|
updateFileOrderFromPages: (pages: PDFPage[]) => void;
|
||||||
|
|
||||||
// Track mutation source to prevent feedback loops
|
|
||||||
lastReorderSource: 'file' | 'page' | null;
|
|
||||||
clearReorderSource: () => void;
|
|
||||||
|
|
||||||
// Sync with FileContext when files change
|
|
||||||
syncWithFileContext: (fileContextFiles: Array<{ fileId: FileId; name: string; versionNumber?: number }>) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PageEditorContext = createContext<PageEditorContextValue | undefined>(undefined);
|
const PageEditorContext = createContext<PageEditorContextValue | undefined>(undefined);
|
||||||
|
|
||||||
interface PageEditorProviderProps {
|
interface PageEditorProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
initialFileIds?: FileId[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PageEditorProvider({ children, initialFileIds = [] }: PageEditorProviderProps) {
|
export function PageEditorProvider({ children }: PageEditorProviderProps) {
|
||||||
// Single array of files with selection state
|
|
||||||
const [files, setFiles] = useState<PageEditorFile[]>([]);
|
|
||||||
const [currentPages, setCurrentPages] = useState<PDFPage[] | null>(null);
|
const [currentPages, setCurrentPages] = useState<PDFPage[] | null>(null);
|
||||||
const [reorderedPages, setReorderedPages] = useState<PDFPage[] | null>(null);
|
const [reorderedPages, setReorderedPages] = useState<PDFPage[] | null>(null);
|
||||||
const [lastReorderSource, setLastReorderSource] = useState<'file' | 'page' | null>(null);
|
|
||||||
const lastReorderSourceAtRef = React.useRef<number>(0);
|
// Page editor's own file order (independent of FileContext)
|
||||||
|
const [fileOrder, setFileOrder] = useState<FileId[]>([]);
|
||||||
|
|
||||||
|
// Read from FileContext (for file metadata only, not order)
|
||||||
const { actions: fileActions } = useFileActions();
|
const { actions: fileActions } = useFileActions();
|
||||||
|
const { state } = useFileState();
|
||||||
|
|
||||||
|
// Keep a ref to always read latest state in stable callbacks
|
||||||
|
const stateRef = React.useRef(state);
|
||||||
|
React.useEffect(() => {
|
||||||
|
stateRef.current = state;
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
// Initialize fileOrder from FileContext when files change (add/remove only)
|
||||||
|
React.useEffect(() => {
|
||||||
|
const currentFileIds = state.files.ids;
|
||||||
|
|
||||||
|
// Add new files to the end
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}, [state.files.ids, fileOrder]);
|
||||||
|
|
||||||
const updateCurrentPages = useCallback((pages: PDFPage[] | null) => {
|
const updateCurrentPages = useCallback((pages: PDFPage[] | null) => {
|
||||||
setCurrentPages(pages);
|
setCurrentPages(pages);
|
||||||
@ -162,198 +174,132 @@ export function PageEditorProvider({ children, initialFileIds = [] }: PageEditor
|
|||||||
setReorderedPages(null);
|
setReorderedPages(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const clearReorderSource = useCallback(() => {
|
|
||||||
setLastReorderSource(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setFileSelection = useCallback((fileId: FileId, selected: boolean) => {
|
const setFileSelection = useCallback((fileId: FileId, selected: boolean) => {
|
||||||
setFiles(prev => {
|
const currentSelection = stateRef.current.ui.selectedFileIds;
|
||||||
const selectedCount = prev.filter(f => f.isSelected).length;
|
const isAlreadySelected = currentSelection.includes(fileId);
|
||||||
|
|
||||||
// Check if we're trying to select when at limit
|
// Check if we're trying to select when at limit
|
||||||
if (selected && selectedCount >= MAX_PAGE_EDITOR_FILES) {
|
if (selected && !isAlreadySelected && currentSelection.length >= MAX_PAGE_EDITOR_FILES) {
|
||||||
const alreadySelected = prev.find(f => f.fileId === fileId)?.isSelected;
|
console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Cannot select more files.`);
|
||||||
if (!alreadySelected) {
|
|
||||||
console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Cannot select more files.`);
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return prev.map(f =>
|
|
||||||
f.fileId === fileId ? { ...f, isSelected: selected } : f
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleFileSelection = useCallback((fileId: FileId) => {
|
|
||||||
setFiles(prev => {
|
|
||||||
const file = prev.find(f => f.fileId === fileId);
|
|
||||||
if (!file) return prev;
|
|
||||||
|
|
||||||
const selectedCount = prev.filter(f => f.isSelected).length;
|
|
||||||
|
|
||||||
// If toggling on and at limit, don't allow
|
|
||||||
if (!file.isSelected && selectedCount >= MAX_PAGE_EDITOR_FILES) {
|
|
||||||
console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Cannot select more files.`);
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
return prev.map(f =>
|
|
||||||
f.fileId === fileId ? { ...f, isSelected: !f.isSelected } : f
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const selectAll = useCallback(() => {
|
|
||||||
setFiles(prev => {
|
|
||||||
if (prev.length > MAX_PAGE_EDITOR_FILES) {
|
|
||||||
console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Only first ${MAX_PAGE_EDITOR_FILES} files will be selected.`);
|
|
||||||
return prev.map((f, index) => ({ ...f, isSelected: index < MAX_PAGE_EDITOR_FILES }));
|
|
||||||
}
|
|
||||||
return prev.map(f => ({ ...f, isSelected: true }));
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const deselectAll = useCallback(() => {
|
|
||||||
setFiles(prev => prev.map(f => ({ ...f, isSelected: false })));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const reorderFiles = useCallback((fromIndex: number, toIndex: number) => {
|
|
||||||
let newFileIds: FileId[] = [];
|
|
||||||
let reorderedPagesResult: PDFPage[] | null = null;
|
|
||||||
|
|
||||||
// Mark that this reorder came from file-level action
|
|
||||||
setLastReorderSource('file');
|
|
||||||
lastReorderSourceAtRef.current = Date.now();
|
|
||||||
|
|
||||||
setFiles(prev => {
|
|
||||||
// Simple array reordering
|
|
||||||
const newOrder = [...prev];
|
|
||||||
const [movedFile] = newOrder.splice(fromIndex, 1);
|
|
||||||
newOrder.splice(toIndex, 0, movedFile);
|
|
||||||
|
|
||||||
// Collect file IDs for later FileContext update
|
|
||||||
newFileIds = newOrder.map(f => f.fileId);
|
|
||||||
|
|
||||||
// If current pages available, reorder them based on file move
|
|
||||||
if (currentPages && currentPages.length > 0 && fromIndex !== toIndex) {
|
|
||||||
// Get the current file order from pages (files that have pages loaded)
|
|
||||||
const currentFileOrder: FileId[] = [];
|
|
||||||
const filesSeen = new Set<FileId>();
|
|
||||||
currentPages.forEach(page => {
|
|
||||||
const fileId = page.originalFileId;
|
|
||||||
if (fileId && !filesSeen.has(fileId)) {
|
|
||||||
filesSeen.add(fileId);
|
|
||||||
currentFileOrder.push(fileId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the moved and target file IDs
|
|
||||||
const movedFileId = prev[fromIndex].fileId;
|
|
||||||
const targetFileId = prev[toIndex].fileId;
|
|
||||||
|
|
||||||
// Find their positions in the current page order (not the full file list)
|
|
||||||
const pageOrderFromIndex = currentFileOrder.findIndex(id => id === movedFileId);
|
|
||||||
const pageOrderToIndex = currentFileOrder.findIndex(id => id === targetFileId);
|
|
||||||
|
|
||||||
// Only reorder pages if both files have pages loaded
|
|
||||||
if (pageOrderFromIndex >= 0 && pageOrderToIndex >= 0) {
|
|
||||||
reorderedPagesResult = reorderPagesForFileMove(currentPages, pageOrderFromIndex, pageOrderToIndex, currentFileOrder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newOrder;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update FileContext after state settles
|
|
||||||
if (newFileIds.length > 0) {
|
|
||||||
fileActions.reorderFiles(newFileIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update reordered pages after state settles
|
|
||||||
if (reorderedPagesResult) {
|
|
||||||
setReorderedPages(reorderedPagesResult);
|
|
||||||
}
|
|
||||||
}, [fileActions, currentPages]);
|
|
||||||
|
|
||||||
const updateFileOrderFromPages = useCallback((pages: PDFPage[]) => {
|
|
||||||
if (!pages || pages.length === 0) return;
|
|
||||||
// Suppress page-derived reorder if a recent explicit file reorder just occurred (prevents feedback loop)
|
|
||||||
if (lastReorderSource === 'file' && Date.now() - lastReorderSourceAtRef.current < 500) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLastReorderSource('page');
|
// Update FileContext selection
|
||||||
lastReorderSourceAtRef.current = Date.now();
|
const newSelectedIds = selected
|
||||||
|
? [...currentSelection, fileId]
|
||||||
|
: currentSelection.filter(id => id !== fileId);
|
||||||
|
|
||||||
|
fileActions.setSelectedFiles(newSelectedIds);
|
||||||
|
}, [fileActions]);
|
||||||
|
|
||||||
|
const toggleFileSelection = useCallback((fileId: FileId) => {
|
||||||
|
const currentSelection = stateRef.current.ui.selectedFileIds;
|
||||||
|
const isCurrentlySelected = currentSelection.includes(fileId);
|
||||||
|
|
||||||
|
// If toggling on and at limit, don't allow
|
||||||
|
if (!isCurrentlySelected && currentSelection.length >= MAX_PAGE_EDITOR_FILES) {
|
||||||
|
console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Cannot select more files.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update FileContext selection
|
||||||
|
const newSelectedIds = isCurrentlySelected
|
||||||
|
? currentSelection.filter(id => id !== fileId)
|
||||||
|
: [...currentSelection, fileId];
|
||||||
|
|
||||||
|
fileActions.setSelectedFiles(newSelectedIds);
|
||||||
|
}, [fileActions]);
|
||||||
|
|
||||||
|
const selectAll = useCallback(() => {
|
||||||
|
const allFileIds = stateRef.current.files.ids;
|
||||||
|
|
||||||
|
if (allFileIds.length > MAX_PAGE_EDITOR_FILES) {
|
||||||
|
console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Only first ${MAX_PAGE_EDITOR_FILES} files will be selected.`);
|
||||||
|
fileActions.setSelectedFiles(allFileIds.slice(0, MAX_PAGE_EDITOR_FILES));
|
||||||
|
} else {
|
||||||
|
fileActions.setSelectedFiles(allFileIds);
|
||||||
|
}
|
||||||
|
}, [fileActions]);
|
||||||
|
|
||||||
|
const deselectAll = useCallback(() => {
|
||||||
|
fileActions.setSelectedFiles([]);
|
||||||
|
}, [fileActions]);
|
||||||
|
|
||||||
|
const reorderFiles = useCallback((fromIndex: number, toIndex: number) => {
|
||||||
|
// Reorder local fileOrder array (page editor workspace only)
|
||||||
|
const newOrder = [...fileOrder];
|
||||||
|
const [movedFileId] = newOrder.splice(fromIndex, 1);
|
||||||
|
newOrder.splice(toIndex, 0, movedFileId);
|
||||||
|
setFileOrder(newOrder);
|
||||||
|
|
||||||
|
// If current pages available, reorder them based on file move
|
||||||
|
if (currentPages && currentPages.length > 0 && fromIndex !== toIndex) {
|
||||||
|
// Get the current file order from pages (files that have pages loaded)
|
||||||
|
const currentFileOrder: FileId[] = [];
|
||||||
|
const filesSeen = new Set<FileId>();
|
||||||
|
currentPages.forEach(page => {
|
||||||
|
const fileId = page.originalFileId;
|
||||||
|
if (fileId && !filesSeen.has(fileId)) {
|
||||||
|
filesSeen.add(fileId);
|
||||||
|
currentFileOrder.push(fileId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the moved and target file IDs
|
||||||
|
const movedFileId = fileOrder[fromIndex];
|
||||||
|
const targetFileId = fileOrder[toIndex];
|
||||||
|
|
||||||
|
// Find their positions in the current page order (not the full file list)
|
||||||
|
const pageOrderFromIndex = currentFileOrder.findIndex(id => id === movedFileId);
|
||||||
|
const pageOrderToIndex = currentFileOrder.findIndex(id => id === targetFileId);
|
||||||
|
|
||||||
|
// Only reorder pages if both files have pages loaded
|
||||||
|
if (pageOrderFromIndex >= 0 && pageOrderToIndex >= 0) {
|
||||||
|
const reorderedPagesResult = reorderPagesForFileMove(currentPages, pageOrderFromIndex, pageOrderToIndex, currentFileOrder);
|
||||||
|
setReorderedPages(reorderedPagesResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [fileOrder, currentPages]);
|
||||||
|
|
||||||
|
const updateFileOrderFromPages = useCallback((pages: PDFPage[]) => {
|
||||||
|
if (!pages || pages.length === 0) return;
|
||||||
|
|
||||||
// Compute the new file order based on page positions
|
// Compute the new file order based on page positions
|
||||||
const newFileOrder = computeFileOrderFromPages(pages);
|
const newFileOrder = computeFileOrderFromPages(pages);
|
||||||
|
|
||||||
if (newFileOrder.length > 0) {
|
if (newFileOrder.length > 0) {
|
||||||
// Update global FileContext order
|
// Update local page editor file order (not FileContext)
|
||||||
fileActions.reorderFiles(newFileOrder);
|
setFileOrder(newFileOrder);
|
||||||
}
|
}
|
||||||
}, [fileActions, lastReorderSource]);
|
|
||||||
|
|
||||||
const syncWithFileContext = useCallback((fileContextFiles: Array<{ fileId: FileId; name: string; versionNumber?: number }>) => {
|
|
||||||
setFiles(prev => {
|
|
||||||
// Create a map of existing files for quick lookup
|
|
||||||
const existingMap = new Map(prev.map(f => [f.fileId, f]));
|
|
||||||
|
|
||||||
// Build new files array from FileContext, preserving selection state
|
|
||||||
const newFiles: PageEditorFile[] = fileContextFiles.map(file => {
|
|
||||||
const existing = existingMap.get(file.fileId);
|
|
||||||
return {
|
|
||||||
fileId: file.fileId,
|
|
||||||
name: file.name,
|
|
||||||
versionNumber: file.versionNumber,
|
|
||||||
isSelected: existing?.isSelected ?? false, // Preserve selection or default to false
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// If no files selected, select all by default (up to MAX_PAGE_EDITOR_FILES)
|
|
||||||
const selectedCount = newFiles.filter(f => f.isSelected).length;
|
|
||||||
if (selectedCount === 0 && newFiles.length > 0) {
|
|
||||||
const maxToSelect = Math.min(newFiles.length, MAX_PAGE_EDITOR_FILES);
|
|
||||||
if (newFiles.length > MAX_PAGE_EDITOR_FILES) {
|
|
||||||
console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Only first ${MAX_PAGE_EDITOR_FILES} files will be selected.`);
|
|
||||||
}
|
|
||||||
return newFiles.map((f, index) => ({
|
|
||||||
...f,
|
|
||||||
isSelected: index < maxToSelect,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce maximum file limit
|
|
||||||
if (selectedCount > MAX_PAGE_EDITOR_FILES) {
|
|
||||||
console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Limiting selection.`);
|
|
||||||
let selectedSoFar = 0;
|
|
||||||
return newFiles.map(f => ({
|
|
||||||
...f,
|
|
||||||
isSelected: f.isSelected && selectedSoFar++ < MAX_PAGE_EDITOR_FILES,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return newFiles;
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const value: PageEditorContextValue = {
|
|
||||||
files,
|
const value: PageEditorContextValue = useMemo(() => ({
|
||||||
currentPages,
|
currentPages,
|
||||||
updateCurrentPages,
|
updateCurrentPages,
|
||||||
reorderedPages,
|
reorderedPages,
|
||||||
clearReorderedPages,
|
clearReorderedPages,
|
||||||
|
fileOrder,
|
||||||
|
setFileOrder,
|
||||||
setFileSelection,
|
setFileSelection,
|
||||||
toggleFileSelection,
|
toggleFileSelection,
|
||||||
selectAll,
|
selectAll,
|
||||||
deselectAll,
|
deselectAll,
|
||||||
reorderFiles,
|
reorderFiles,
|
||||||
updateFileOrderFromPages,
|
updateFileOrderFromPages,
|
||||||
lastReorderSource,
|
}), [
|
||||||
clearReorderSource,
|
currentPages,
|
||||||
syncWithFileContext,
|
updateCurrentPages,
|
||||||
};
|
reorderedPages,
|
||||||
|
clearReorderedPages,
|
||||||
|
fileOrder,
|
||||||
|
setFileSelection,
|
||||||
|
toggleFileSelection,
|
||||||
|
selectAll,
|
||||||
|
deselectAll,
|
||||||
|
reorderFiles,
|
||||||
|
updateFileOrderFromPages,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageEditorContext.Provider value={value}>
|
<PageEditorContext.Provider value={value}>
|
||||||
|
|||||||
@ -149,17 +149,13 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
|||||||
|
|
||||||
// Validate that all IDs exist in current state
|
// Validate that all IDs exist in current state
|
||||||
const validIds = orderedFileIds.filter(id => state.files.byId[id]);
|
const validIds = orderedFileIds.filter(id => state.files.byId[id]);
|
||||||
// Reorder selected files by passed order
|
|
||||||
const selectedFileIds = orderedFileIds.filter(id => state.ui.selectedFileIds.includes(id));
|
// Don't touch selectedFileIds - it's just a reference list, order doesn't matter
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
files: {
|
files: {
|
||||||
...state.files,
|
...state.files,
|
||||||
ids: validIds
|
ids: validIds
|
||||||
},
|
|
||||||
ui: {
|
|
||||||
...state.ui,
|
|
||||||
selectedFileIds,
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user