mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-04-22 23:08:53 +02:00
Enhance drag-and-drop functionality with new drop hint resolution and target index calculation; refactor file color mapping in PageEditor and implement dropdown state management for improved file handling.
This commit is contained in:
@@ -29,6 +29,98 @@ interface DragDropGridProps<T extends DragDropItem> {
|
|||||||
zoomLevel?: number;
|
zoomLevel?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DropSide = 'left' | 'right' | null;
|
||||||
|
|
||||||
|
interface DropHint {
|
||||||
|
hoveredId: string | null;
|
||||||
|
dropSide: DropSide;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDropHint(
|
||||||
|
activeId: string | null,
|
||||||
|
itemRefs: React.MutableRefObject<Map<string, HTMLDivElement>>,
|
||||||
|
cursorX: number,
|
||||||
|
cursorY: number,
|
||||||
|
): DropHint {
|
||||||
|
if (!activeId) {
|
||||||
|
return { hoveredId: null, dropSide: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = new Map<number, Array<{ id: string; rect: DOMRect }>>();
|
||||||
|
|
||||||
|
itemRefs.current.forEach((element, itemId) => {
|
||||||
|
if (!element || itemId === activeId) return;
|
||||||
|
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const rowCenter = rect.top + rect.height / 2;
|
||||||
|
|
||||||
|
let row = rows.get(rowCenter);
|
||||||
|
if (!row) {
|
||||||
|
row = [];
|
||||||
|
rows.set(rowCenter, row);
|
||||||
|
}
|
||||||
|
row.push({ id: itemId, rect });
|
||||||
|
});
|
||||||
|
|
||||||
|
let hoveredId: string | null = null;
|
||||||
|
let dropSide: DropSide = null;
|
||||||
|
|
||||||
|
let closestRowY = 0;
|
||||||
|
let closestRowDistance = Infinity;
|
||||||
|
|
||||||
|
rows.forEach((_items, rowY) => {
|
||||||
|
const distance = Math.abs(cursorY - rowY);
|
||||||
|
if (distance < closestRowDistance) {
|
||||||
|
closestRowDistance = distance;
|
||||||
|
closestRowY = rowY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const closestRow = rows.get(closestRowY);
|
||||||
|
if (!closestRow || closestRow.length === 0) {
|
||||||
|
return { hoveredId: null, dropSide: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
let closestDistance = Infinity;
|
||||||
|
closestRow.forEach(({ id, rect }) => {
|
||||||
|
const distanceToLeft = Math.abs(cursorX - rect.left);
|
||||||
|
const distanceToRight = Math.abs(cursorX - rect.right);
|
||||||
|
|
||||||
|
if (distanceToLeft < closestDistance) {
|
||||||
|
closestDistance = distanceToLeft;
|
||||||
|
hoveredId = id;
|
||||||
|
dropSide = 'left';
|
||||||
|
}
|
||||||
|
if (distanceToRight < closestDistance) {
|
||||||
|
closestDistance = distanceToRight;
|
||||||
|
hoveredId = id;
|
||||||
|
dropSide = 'right';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { hoveredId, dropSide };
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTargetIndex<T extends DragDropItem>(
|
||||||
|
hoveredId: string | null,
|
||||||
|
dropSide: DropSide,
|
||||||
|
items: T[],
|
||||||
|
fallbackIndex: number | null,
|
||||||
|
): number | null {
|
||||||
|
if (hoveredId) {
|
||||||
|
const hoveredIndex = items.findIndex(item => item.id === hoveredId);
|
||||||
|
if (hoveredIndex !== -1) {
|
||||||
|
return hoveredIndex + (dropSide === 'right' ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallbackIndex !== null && fallbackIndex !== undefined) {
|
||||||
|
return fallbackIndex + (dropSide === 'right' ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Lightweight wrapper that handles dnd-kit hooks for each visible item
|
// Lightweight wrapper that handles dnd-kit hooks for each visible item
|
||||||
interface DraggableItemProps<T extends DragDropItem> {
|
interface DraggableItemProps<T extends DragDropItem> {
|
||||||
item: T;
|
item: T;
|
||||||
@@ -126,7 +218,7 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
const [activeId, setActiveId] = useState<string | null>(null);
|
||||||
const [dragPreview, setDragPreview] = useState<{ src: string; rotation: number } | null>(null);
|
const [dragPreview, setDragPreview] = useState<{ src: string; rotation: number } | null>(null);
|
||||||
const [hoveredItemId, setHoveredItemId] = useState<string | null>(null);
|
const [hoveredItemId, setHoveredItemId] = useState<string | null>(null);
|
||||||
const [dropSide, setDropSide] = useState<'left' | 'right' | null>(null);
|
const [dropSide, setDropSide] = useState<DropSide>(null);
|
||||||
|
|
||||||
// Configure sensors for dnd-kit with activation constraint
|
// Configure sensors for dnd-kit with activation constraint
|
||||||
// Require 10px movement before drag starts to allow clicks for selection
|
// Require 10px movement before drag starts to allow clicks for selection
|
||||||
@@ -156,74 +248,9 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
|
|
||||||
if (rafId === null) {
|
if (rafId === null) {
|
||||||
rafId = requestAnimationFrame(() => {
|
rafId = requestAnimationFrame(() => {
|
||||||
// Step 1: Group items by rows and find closest row to cursor
|
const hint = resolveDropHint(activeId, itemRefs, cursorX, cursorY);
|
||||||
const rows = new Map<number, Array<{ id: string; element: HTMLDivElement; rect: DOMRect }>>();
|
setHoveredItemId(hint.hoveredId);
|
||||||
|
setDropSide(hint.dropSide);
|
||||||
itemRefs.current.forEach((element, itemId) => {
|
|
||||||
// Skip the item being dragged
|
|
||||||
if (itemId === activeId) return;
|
|
||||||
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
const rowCenter = rect.top + rect.height / 2;
|
|
||||||
|
|
||||||
// Group items by their vertical center position (items in same row will have similar centers)
|
|
||||||
let foundRow = false;
|
|
||||||
rows.forEach((items, rowY) => {
|
|
||||||
if (Math.abs(rowY - rowCenter) < rect.height / 4) {
|
|
||||||
items.push({ id: itemId, element, rect });
|
|
||||||
foundRow = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!foundRow) {
|
|
||||||
rows.set(rowCenter, [{ id: itemId, element, rect }]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Step 2: Find the closest row to cursor Y position
|
|
||||||
let closestRowY = 0;
|
|
||||||
let closestRowDistance = Infinity;
|
|
||||||
Array.from(rows.keys()).forEach((rowY) => {
|
|
||||||
const distance = Math.abs(cursorY - rowY);
|
|
||||||
if (distance < closestRowDistance) {
|
|
||||||
closestRowDistance = distance;
|
|
||||||
closestRowY = rowY;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const closestRow = rows.get(closestRowY);
|
|
||||||
if (!closestRow || closestRow.length === 0) {
|
|
||||||
setHoveredItemId(null);
|
|
||||||
setDropSide(null);
|
|
||||||
rafId = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Within the closest row, find the closest edge to cursor X position
|
|
||||||
let closestItemId: string | null = null;
|
|
||||||
let closestDistance = Infinity;
|
|
||||||
let closestSide: 'left' | 'right' = 'left';
|
|
||||||
|
|
||||||
closestRow.forEach(({ id, rect }) => {
|
|
||||||
// Calculate distance to left and right edges
|
|
||||||
const distanceToLeft = Math.abs(cursorX - rect.left);
|
|
||||||
const distanceToRight = Math.abs(cursorX - rect.right);
|
|
||||||
|
|
||||||
// Find the closest edge
|
|
||||||
if (distanceToLeft < closestDistance) {
|
|
||||||
closestDistance = distanceToLeft;
|
|
||||||
closestItemId = id;
|
|
||||||
closestSide = 'left';
|
|
||||||
}
|
|
||||||
if (distanceToRight < closestDistance) {
|
|
||||||
closestDistance = distanceToRight;
|
|
||||||
closestItemId = id;
|
|
||||||
closestSide = 'right';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setHoveredItemId(closestItemId);
|
|
||||||
setDropSide(closestSide);
|
|
||||||
rafId = null;
|
rafId = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -442,21 +469,13 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
|
|
||||||
const sourcePageNumber = activeData.pageNumber;
|
const sourcePageNumber = activeData.pageNumber;
|
||||||
|
|
||||||
let targetIndex: number | null = null;
|
const overData = over?.data.current;
|
||||||
|
const targetIndex = resolveTargetIndex(
|
||||||
if (hoveredItemId) {
|
hoveredItemId,
|
||||||
const hoveredIndex = visibleItems.findIndex(item => item.id === hoveredItemId);
|
finalDropSide,
|
||||||
if (hoveredIndex !== -1) {
|
visibleItems,
|
||||||
targetIndex = hoveredIndex + (finalDropSide === 'right' ? 1 : 0);
|
overData ? overData.index : null
|
||||||
}
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (targetIndex === null && over) {
|
|
||||||
const overData = over.data.current;
|
|
||||||
if (overData) {
|
|
||||||
targetIndex = overData.index + (finalDropSide === 'right' ? 1 : 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetIndex === null) return;
|
if (targetIndex === null) return;
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { usePageDocument } from './hooks/usePageDocument';
|
|||||||
import { usePageEditorState } from './hooks/usePageEditorState';
|
import { usePageEditorState } from './hooks/usePageEditorState';
|
||||||
import { parseSelection } from "../../utils/bulkselection/parseSelection";
|
import { parseSelection } from "../../utils/bulkselection/parseSelection";
|
||||||
import { usePageEditorRightRailButtons } from "./pageEditorRightRailButtons";
|
import { usePageEditorRightRailButtons } from "./pageEditorRightRailButtons";
|
||||||
|
import { useFileColorMap } from "./hooks/useFileColorMap";
|
||||||
|
|
||||||
export interface PageEditorProps {
|
export interface PageEditorProps {
|
||||||
onFunctionsReady?: (functions: PageEditorFunctions) => void;
|
onFunctionsReady?: (functions: PageEditorFunctions) => void;
|
||||||
@@ -981,30 +982,7 @@ const PageEditor = ({
|
|||||||
const displayedPages = displayDocument?.pages || [];
|
const displayedPages = displayDocument?.pages || [];
|
||||||
|
|
||||||
// Track color assignments by insertion order (files keep their color)
|
// Track color assignments by insertion order (files keep their color)
|
||||||
const fileColorAssignments = useRef(new Map<FileId, number>());
|
const fileColorIndexMap = useFileColorMap(orderedFileIds);
|
||||||
|
|
||||||
// Create a stable mapping of fileId to color index (preserves colors on reorder)
|
|
||||||
const fileColorIndexMap = useMemo(() => {
|
|
||||||
const assignments = fileColorAssignments.current;
|
|
||||||
|
|
||||||
// Remove colors for files that no longer exist
|
|
||||||
const activeIds = new Set(orderedFileIds);
|
|
||||||
for (const fileId of Array.from(assignments.keys())) {
|
|
||||||
if (!activeIds.has(fileId)) {
|
|
||||||
assignments.delete(fileId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign colors to new files based on insertion order
|
|
||||||
orderedFileIds.forEach(fileId => {
|
|
||||||
if (!assignments.has(fileId)) {
|
|
||||||
assignments.set(fileId, assignments.size);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up removed files (only remove files that are completely gone, not just deselected)
|
|
||||||
return assignments;
|
|
||||||
}, [orderedFileIds.join(',')]); // Only recalculate when the set of files changes, not the order
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
34
frontend/src/components/pageEditor/hooks/useFileColorMap.ts
Normal file
34
frontend/src/components/pageEditor/hooks/useFileColorMap.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useMemo, useRef } from 'react';
|
||||||
|
import { FileId } from '../../../types/file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maintains stable color assignments for a collection of file IDs.
|
||||||
|
* Colors are assigned by insertion order and preserved across reorders.
|
||||||
|
*/
|
||||||
|
export function useFileColorMap(fileIds: FileId[]): Map<FileId, number> {
|
||||||
|
const assignmentsRef = useRef(new Map<FileId, number>());
|
||||||
|
|
||||||
|
const serializedIds = useMemo(() => fileIds.join(','), [fileIds]);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const assignments = assignmentsRef.current;
|
||||||
|
const activeIds = new Set(fileIds);
|
||||||
|
|
||||||
|
// Remove colors for files that no longer exist
|
||||||
|
for (const id of Array.from(assignments.keys())) {
|
||||||
|
if (!activeIds.has(id)) {
|
||||||
|
assignments.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign colors to any new files
|
||||||
|
fileIds.forEach((id) => {
|
||||||
|
if (!assignments.has(id)) {
|
||||||
|
assignments.set(id, assignments.size);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return assignments;
|
||||||
|
}, [serializedIds, fileIds]);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { usePageEditor } from '../../../contexts/PageEditorContext';
|
||||||
|
import { useFileState } from '../../../contexts/FileContext';
|
||||||
|
import { FileId } from '../../../types/file';
|
||||||
|
import { useFileColorMap } from './useFileColorMap';
|
||||||
|
|
||||||
|
export interface PageEditorDropdownFile {
|
||||||
|
fileId: FileId;
|
||||||
|
name: string;
|
||||||
|
versionNumber?: number;
|
||||||
|
isSelected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageEditorDropdownState {
|
||||||
|
files: PageEditorDropdownFile[];
|
||||||
|
selectedCount: number;
|
||||||
|
totalCount: number;
|
||||||
|
onToggleSelection: (fileId: FileId) => void;
|
||||||
|
onReorder: (fromIndex: number, toIndex: number) => void;
|
||||||
|
fileColorMap: Map<FileId, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPdf = (name?: string | null) =>
|
||||||
|
typeof name === 'string' && name.toLowerCase().endsWith('.pdf');
|
||||||
|
|
||||||
|
export function usePageEditorDropdownState(): PageEditorDropdownState {
|
||||||
|
const { state, selectors } = useFileState();
|
||||||
|
const {
|
||||||
|
toggleFileSelection,
|
||||||
|
reorderFiles,
|
||||||
|
fileOrder,
|
||||||
|
} = usePageEditor();
|
||||||
|
|
||||||
|
const pageEditorFiles = useMemo(() => {
|
||||||
|
return fileOrder
|
||||||
|
.map<PageEditorDropdownFile | null>((fileId) => {
|
||||||
|
const stub = selectors.getStirlingFileStub(fileId);
|
||||||
|
if (!isPdf(stub?.name)) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
fileId,
|
||||||
|
name: stub?.name || '',
|
||||||
|
versionNumber: stub?.versionNumber,
|
||||||
|
isSelected: state.ui.selectedFileIds.includes(fileId),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((file): file is PageEditorDropdownFile => file !== null);
|
||||||
|
}, [fileOrder, selectors, state.ui.selectedFileIds]);
|
||||||
|
|
||||||
|
const fileColorMap = useFileColorMap(pageEditorFiles.map((file) => file.fileId));
|
||||||
|
|
||||||
|
const selectedCount = useMemo(
|
||||||
|
() => pageEditorFiles.filter((file) => file.isSelected).length,
|
||||||
|
[pageEditorFiles]
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo<PageEditorDropdownState>(() => ({
|
||||||
|
files: pageEditorFiles,
|
||||||
|
selectedCount,
|
||||||
|
totalCount: pageEditorFiles.length,
|
||||||
|
onToggleSelection: toggleFileSelection,
|
||||||
|
onReorder: reorderFiles,
|
||||||
|
fileColorMap,
|
||||||
|
}), [pageEditorFiles, selectedCount, toggleFileSelection, reorderFiles, fileColorMap]);
|
||||||
|
}
|
||||||
@@ -8,29 +8,9 @@ import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
|||||||
import { LocalIcon } from "./LocalIcon";
|
import { LocalIcon } from "./LocalIcon";
|
||||||
import { WorkbenchType, isValidWorkbench } from '../../types/workbench';
|
import { WorkbenchType, isValidWorkbench } from '../../types/workbench';
|
||||||
import { PageEditorFileDropdown } from './PageEditorFileDropdown';
|
import { PageEditorFileDropdown } from './PageEditorFileDropdown';
|
||||||
import { usePageEditor } from '../../contexts/PageEditorContext';
|
|
||||||
import { useFileState } from '../../contexts/FileContext';
|
|
||||||
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
|
||||||
import { FileId } from '../../types/file';
|
|
||||||
import type { CustomWorkbenchViewInstance } from '../../contexts/ToolWorkflowContext';
|
import type { CustomWorkbenchViewInstance } from '../../contexts/ToolWorkflowContext';
|
||||||
import { FileDropdownMenu } from './FileDropdownMenu';
|
import { FileDropdownMenu } from './FileDropdownMenu';
|
||||||
|
import { usePageEditorDropdownState, PageEditorDropdownState } from '../pageEditor/hooks/usePageEditorDropdownState';
|
||||||
// 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>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewOptionStyle: React.CSSProperties = {
|
const viewOptionStyle: React.CSSProperties = {
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
@@ -48,7 +28,7 @@ const createViewOptions = (
|
|||||||
activeFiles: Array<{ fileId: string; 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?: PageEditorDropdownState,
|
||||||
customViews?: CustomWorkbenchViewInstance[]
|
customViews?: CustomWorkbenchViewInstance[]
|
||||||
) => {
|
) => {
|
||||||
// Viewer dropdown logic
|
// Viewer dropdown logic
|
||||||
@@ -169,115 +149,7 @@ const TopControls = ({
|
|||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
|
const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
|
||||||
|
|
||||||
// Get FileContext state and PageEditor coordination functions
|
const pageEditorState = usePageEditorDropdownState();
|
||||||
const { state, selectors } = useFileState();
|
|
||||||
const pageEditorContext = usePageEditor();
|
|
||||||
const {
|
|
||||||
toggleFileSelection,
|
|
||||||
reorderFiles: pageEditorReorderFiles,
|
|
||||||
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
|
|
||||||
const selectedCount = pageEditorFiles?.filter(f => f.isSelected).length || 0;
|
|
||||||
const totalCount = pageEditorFiles?.length || 0;
|
|
||||||
|
|
||||||
// Create stable file IDs string for dependency (only changes when file set changes)
|
|
||||||
const fileIdsString = (pageEditorFiles || []).map(f => f.fileId).sort().join(',');
|
|
||||||
|
|
||||||
// Track color assignments by insertion order (files keep their color)
|
|
||||||
const fileColorAssignments = React.useRef(new Map<string, number>());
|
|
||||||
|
|
||||||
// Create stable file color mapping (preserves colors on reorder)
|
|
||||||
const fileColorMap = useMemo(() => {
|
|
||||||
const map = new Map<string, number>();
|
|
||||||
if (!pageEditorFiles || pageEditorFiles.length === 0) return map;
|
|
||||||
|
|
||||||
const allFileIds = (pageEditorFiles || []).map(f => f.fileId as string);
|
|
||||||
|
|
||||||
// Assign colors to new files based on insertion order
|
|
||||||
allFileIds.forEach(fileId => {
|
|
||||||
if (!fileColorAssignments.current.has(fileId)) {
|
|
||||||
fileColorAssignments.current.set(fileId, fileColorAssignments.current.size);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up removed files
|
|
||||||
const activeSet = new Set(allFileIds);
|
|
||||||
for (const fileId of fileColorAssignments.current.keys()) {
|
|
||||||
if (!activeSet.has(fileId)) {
|
|
||||||
fileColorAssignments.current.delete(fileId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileColorAssignments.current;
|
|
||||||
}, [fileIdsString]);
|
|
||||||
|
|
||||||
// Get pageEditorFunctions from ToolWorkflowContext
|
|
||||||
const { pageEditorFunctions } = useToolWorkflow();
|
|
||||||
|
|
||||||
// Memoize the reorder handler
|
|
||||||
const handleReorder = useCallback((fromIndex: number, toIndex: number) => {
|
|
||||||
// Single source of truth: PageEditorContext handles file->page reorder propagation
|
|
||||||
pageEditorReorderFiles(fromIndex, toIndex);
|
|
||||||
}, [pageEditorReorderFiles]);
|
|
||||||
|
|
||||||
const handleViewChange = useCallback((view: string) => {
|
const handleViewChange = useCallback((view: string) => {
|
||||||
if (!isValidWorkbench(view)) {
|
if (!isValidWorkbench(view)) {
|
||||||
@@ -301,16 +173,6 @@ const TopControls = ({
|
|||||||
});
|
});
|
||||||
}, [setCurrentView]);
|
}, [setCurrentView]);
|
||||||
|
|
||||||
// Memoize pageEditorState object to prevent recreating on every render
|
|
||||||
const pageEditorState = useMemo<PageEditorState>(() => ({
|
|
||||||
files: pageEditorFiles,
|
|
||||||
selectedCount,
|
|
||||||
totalCount,
|
|
||||||
onToggleSelection: toggleFileSelection,
|
|
||||||
onReorder: handleReorder,
|
|
||||||
fileColorMap,
|
|
||||||
}), [pageEditorFiles, selectedCount, totalCount, toggleFileSelection, handleReorder, fileColorMap]);
|
|
||||||
|
|
||||||
// Memoize view options to prevent SegmentedControl re-renders
|
// Memoize view options to prevent SegmentedControl re-renders
|
||||||
const viewOptions = useMemo(() => createViewOptions(
|
const viewOptions = useMemo(() => createViewOptions(
|
||||||
currentView,
|
currentView,
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { TooltipContent } from '../../types/tips';
|
import { TooltipContent } from '../../types/tips';
|
||||||
import { SPLIT_METHODS, type SplitMethod } from '../../constants/splitConstants';
|
import { SPLIT_METHODS, type SplitMethod } from '../../constants/splitConstants';
|
||||||
|
|
||||||
export const useSplitSettingsTips = (method: SplitMethod | ''): TooltipContent | null => {
|
/**
|
||||||
|
* Hook that returns tooltip content for ALL split methods
|
||||||
|
* Can be called once and then looked up by method
|
||||||
|
*/
|
||||||
|
export const useSplitSettingsTips = (): Record<SplitMethod, TooltipContent> => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!method) return null;
|
|
||||||
|
|
||||||
const tooltipMap: Record<SplitMethod, TooltipContent> = {
|
const tooltipMap: Record<SplitMethod, TooltipContent> = {
|
||||||
[SPLIT_METHODS.BY_PAGES]: {
|
[SPLIT_METHODS.BY_PAGES]: {
|
||||||
header: {
|
header: {
|
||||||
@@ -130,5 +132,5 @@ export const useSplitSettingsTips = (method: SplitMethod | ''): TooltipContent |
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return tooltipMap[method];
|
return tooltipMap;
|
||||||
};
|
};
|
||||||
@@ -21,12 +21,17 @@ const Split = (props: BaseToolProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const methodTips = useSplitMethodTips();
|
const methodTips = useSplitMethodTips();
|
||||||
const settingsTips = useSplitSettingsTips(base.params.parameters.method);
|
const allSettingsTips = useSplitSettingsTips();
|
||||||
|
|
||||||
|
// Get tooltip content for the currently selected method
|
||||||
|
const settingsTips = base.params.parameters.method
|
||||||
|
? allSettingsTips[base.params.parameters.method]
|
||||||
|
: null;
|
||||||
|
|
||||||
// Get tooltip content for a specific method
|
// Get tooltip content for a specific method
|
||||||
const getMethodTooltip = (_option: MethodOption) => {
|
const getMethodTooltip = (option: MethodOption) => {
|
||||||
// TODO: Fix hook call in non-React function
|
const tooltipContent = allSettingsTips[option.value];
|
||||||
return [];
|
return tooltipContent?.tips || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the method name for the settings step title
|
// Get the method name for the settings step title
|
||||||
|
|||||||
Reference in New Issue
Block a user