Feature/v2/viewer tabs (#4646)

# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: James Brunton <james@stirlingpdf.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Reece Browne
2025-10-13 15:03:07 +01:00
committed by GitHub
parent 3cebcc70af
commit af57ae02dd
15 changed files with 435 additions and 249 deletions

View File

@@ -45,6 +45,7 @@ const FileEditorThumbnail = ({
selectedFiles,
onToggleFile,
onCloseFile,
onViewFile,
_onSetStatus,
onReorderFiles,
onDownloadFile,
@@ -205,6 +206,11 @@ const FileEditorThumbnail = ({
onToggleFile(file.id);
};
const handleCardDoubleClick = () => {
if (!isSupported) return;
onViewFile(file.id);
};
// ---- Style helpers ----
const getHeaderClassName = () => {
if (hasError) return styles.headerError;
@@ -226,6 +232,7 @@ const FileEditorThumbnail = ({
role="listitem"
aria-selected={isSelected}
onClick={handleCardClick}
onDoubleClick={handleCardDoubleClick}
>
{/* Header bar */}
<div

View File

@@ -1,9 +1,11 @@
import React from 'react';
import { Box } from '@mantine/core';
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
import { useFileHandler } from '../../hooks/useFileHandler';
import { useFileState } from '../../contexts/FileContext';
import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext';
import { useViewer } from '../../contexts/ViewerContext';
import './Workbench.css';
import TopControls from '../shared/TopControls';
@@ -20,11 +22,11 @@ export default function Workbench() {
const { isRainbowMode } = useRainbowThemeContext();
// Use context-based hooks to eliminate all prop drilling
const { state } = useFileState();
const { selectors } = useFileState();
const { workbench: currentView } = useNavigationState();
const { actions: navActions } = useNavigationActions();
const setCurrentView = navActions.setWorkbench;
const activeFiles = state.files.ids;
const activeFiles = selectors.getFiles();
const {
previewFile,
pageEditorFunctions,
@@ -44,6 +46,9 @@ export default function Workbench() {
const selectedTool = selectedToolId ? toolRegistry[selectedToolId] : null;
const { addFiles } = useFileHandler();
// Get active file index from ViewerContext
const { activeFileIndex, setActiveFileIndex } = useViewer();
const handlePreviewClose = () => {
setPreviewFile(null);
const previousMode = sessionStorage.getItem('previousMode');
@@ -95,6 +100,8 @@ export default function Workbench() {
setSidebarsVisible={setSidebarsVisible}
previewFile={previewFile}
onClose={handlePreviewClose}
activeFileIndex={activeFileIndex}
setActiveFileIndex={setActiveFileIndex}
/>
);
@@ -150,6 +157,12 @@ export default function Workbench() {
<TopControls
currentView={currentView}
setCurrentView={setCurrentView}
activeFiles={activeFiles.map(f => {
const stub = selectors.getStirlingFileStub(f.fileId);
return { fileId: f.fileId, name: f.name, versionNumber: stub?.versionNumber };
})}
currentFileIndex={activeFileIndex}
onFileSelect={setActiveFileIndex}
/>
)}
@@ -161,7 +174,7 @@ export default function Workbench() {
className="flex-1 min-h-0 relative z-10 workbench-scrollable "
style={{
transition: 'opacity 0.15s ease-in-out',
paddingTop: activeFiles.length > 0 ? '3.5rem' : '0',
paddingTop: currentView === 'viewer' ? '0' : (activeFiles.length > 0 ? '3.5rem' : '0'),
}}
>
{renderMainContent()}

View File

@@ -0,0 +1,78 @@
import React from 'react';
import { Menu, Loader, Group, Text } from '@mantine/core';
import VisibilityIcon from '@mui/icons-material/Visibility';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import FitText from './FitText';
interface FileDropdownMenuProps {
displayName: string;
activeFiles: Array<{ fileId: string; name: string; versionNumber?: number }>;
currentFileIndex: number;
onFileSelect?: (index: number) => void;
switchingTo?: string | null;
viewOptionStyle: React.CSSProperties;
pillRef?: React.RefObject<HTMLDivElement>;
}
export const FileDropdownMenu: React.FC<FileDropdownMenuProps> = ({
displayName,
activeFiles,
currentFileIndex,
onFileSelect,
switchingTo,
viewOptionStyle,
}) => {
return (
<Menu trigger="click" position="bottom" width="30rem">
<Menu.Target>
<div style={{...viewOptionStyle, cursor: 'pointer'}}>
{switchingTo === "viewer" ? (
<Loader size="xs" />
) : (
<VisibilityIcon fontSize="small" />
)}
<FitText text={displayName} fontSize={14} minimumFontScale={0.6} className="ph-no-capture" />
<KeyboardArrowDownIcon fontSize="small" />
</div>
</Menu.Target>
<Menu.Dropdown style={{
backgroundColor: 'var(--right-rail-bg)',
border: '1px solid var(--border-subtle)',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
maxHeight: '50vh',
overflowY: 'auto'
}}>
{activeFiles.map((file, index) => {
const itemName = file?.name || 'Untitled';
const isActive = index === currentFileIndex;
return (
<Menu.Item
key={file.fileId}
onClick={(e) => {
e.stopPropagation();
onFileSelect?.(index);
}}
className="viewer-file-tab"
{...(isActive && { 'data-active': true })}
style={{
justifyContent: 'flex-start',
}}
>
<Group gap="xs" style={{ width: '100%', justifyContent: 'space-between' }}>
<div style={{ flex: 1, textAlign: 'left', minWidth: 0 }}>
<FitText text={itemName} fontSize={14} minimumFontScale={0.7} className="ph-no-capture" />
</div>
{file.versionNumber && file.versionNumber > 1 && (
<Text size="xs" c="dimmed">
v{file.versionNumber}
</Text>
)}
</Group>
</Menu.Item>
);
})}
</Menu.Dropdown>
</Menu>
);
};

View File

@@ -6,9 +6,10 @@ import VisibilityIcon from "@mui/icons-material/Visibility";
import EditNoteIcon from "@mui/icons-material/EditNote";
import FolderIcon from "@mui/icons-material/Folder";
import { WorkbenchType, isValidWorkbench } from '../../types/workbench';
import { FileDropdownMenu } from './FileDropdownMenu';
const viewOptionStyle = {
const viewOptionStyle: React.CSSProperties = {
display: 'inline-flex',
flexDirection: 'row',
alignItems: 'center',
@@ -19,16 +20,38 @@ const viewOptionStyle = {
// Build view options showing text always
const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null) => {
const createViewOptions = (
currentView: WorkbenchType,
switchingTo: WorkbenchType | null,
activeFiles: Array<{ fileId: string; name: string; versionNumber?: number }>,
currentFileIndex: number,
onFileSelect?: (index: number) => void
) => {
const currentFile = activeFiles[currentFileIndex];
const isInViewer = currentView === 'viewer';
const fileName = currentFile?.name || '';
const displayName = isInViewer && fileName ? fileName : 'Viewer';
const hasMultipleFiles = activeFiles.length > 1;
const showDropdown = isInViewer && hasMultipleFiles;
const viewerOption = {
label: (
<div style={viewOptionStyle as React.CSSProperties}>
label: showDropdown ? (
<FileDropdownMenu
displayName={displayName}
activeFiles={activeFiles}
currentFileIndex={currentFileIndex}
onFileSelect={onFileSelect}
switchingTo={switchingTo}
viewOptionStyle={viewOptionStyle}
/>
) : (
<div style={viewOptionStyle}>
{switchingTo === "viewer" ? (
<Loader size="xs" />
) : (
<VisibilityIcon fontSize="small" />
)}
<span>Viewer</span>
<span className="ph-no-capture">{displayName}</span>
</div>
),
value: "viewer",
@@ -36,7 +59,7 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
const pageEditorOption = {
label: (
<div style={viewOptionStyle as React.CSSProperties}>
<div style={viewOptionStyle}>
{currentView === "pageEditor" ? (
<>
{switchingTo === "pageEditor" ? <Loader size="xs" /> : <EditNoteIcon fontSize="small" />}
@@ -55,7 +78,7 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
const fileEditorOption = {
label: (
<div style={viewOptionStyle as React.CSSProperties}>
<div style={viewOptionStyle}>
{currentView === "fileEditor" ? (
<>
{switchingTo === "fileEditor" ? <Loader size="xs" /> : <FolderIcon fontSize="small" />}
@@ -83,12 +106,18 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
interface TopControlsProps {
currentView: WorkbenchType;
setCurrentView: (view: WorkbenchType) => void;
activeFiles?: Array<{ fileId: string; name: string; versionNumber?: number }>;
currentFileIndex?: number;
onFileSelect?: (index: number) => void;
}
const TopControls = ({
currentView,
setCurrentView,
}: TopControlsProps) => {
activeFiles = [],
currentFileIndex = 0,
onFileSelect,
}: TopControlsProps) => {
const { isRainbowMode } = useRainbowThemeContext();
const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
@@ -118,7 +147,7 @@ const TopControls = ({
<div className="absolute left-0 w-full top-0 z-[100] pointer-events-none">
<div className="flex justify-center mt-[0.5rem]">
<SegmentedControl
data={createViewOptions(currentView, switchingTo)}
data={createViewOptions(currentView, switchingTo, activeFiles, currentFileIndex, onFileSelect)}
value={currentView}
onChange={handleViewChange}
color="blue"

View File

@@ -1,6 +1,5 @@
import React, { useCallback, useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Box, Center, Text, ActionIcon } from '@mantine/core';
import { useMantineTheme, useMantineColorScheme } from '@mantine/core';
import CloseIcon from '@mui/icons-material/Close';
import { useFileState, useFileActions } from "../../contexts/FileContext";
@@ -20,6 +19,8 @@ export interface EmbedPdfViewerProps {
setSidebarsVisible: (v: boolean) => void;
onClose?: () => void;
previewFile?: File | null;
activeFileIndex?: number;
setActiveFileIndex?: (index: number) => void;
}
const EmbedPdfViewerContent = ({
@@ -27,9 +28,9 @@ const EmbedPdfViewerContent = ({
setSidebarsVisible: _setSidebarsVisible,
onClose,
previewFile,
activeFileIndex: externalActiveFileIndex,
setActiveFileIndex: externalSetActiveFileIndex,
}: EmbedPdfViewerProps) => {
const theme = useMantineTheme();
const { colorScheme: _colorScheme } = useMantineColorScheme();
const viewerRef = React.useRef<HTMLDivElement>(null);
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
@@ -52,10 +53,11 @@ const EmbedPdfViewerContent = ({
const { signatureApiRef, historyApiRef } = useSignature();
// Get current file from FileContext
const { selectors } = useFileState();
const { selectors, state } = useFileState();
const { actions } = useFileActions();
const activeFiles = selectors.getFiles();
const activeFileIds = activeFiles.map(f => f.fileId);
const selectedFileIds = state.ui.selectedFileIds;
// Navigation guard for unsaved changes
const { setHasUnsavedChanges, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker } = useNavigationGuard();
@@ -67,15 +69,40 @@ const EmbedPdfViewerContent = ({
// Enable annotations when: in sign mode, OR annotation mode is active, OR we want to show existing annotations
const shouldEnableAnnotations = isSignatureMode || isAnnotationMode || isAnnotationsVisible;
// Track which file tab is active
const [internalActiveFileIndex, setInternalActiveFileIndex] = useState(0);
const activeFileIndex = externalActiveFileIndex ?? internalActiveFileIndex;
const setActiveFileIndex = externalSetActiveFileIndex ?? setInternalActiveFileIndex;
const hasInitializedFromSelection = useRef(false);
// When viewer opens with a selected file, switch to that file
useEffect(() => {
if (!hasInitializedFromSelection.current && selectedFileIds.length > 0 && activeFiles.length > 0) {
const selectedFileId = selectedFileIds[0];
const index = activeFiles.findIndex(f => f.fileId === selectedFileId);
if (index !== -1 && index !== activeFileIndex) {
setActiveFileIndex(index);
}
hasInitializedFromSelection.current = true;
}
}, [selectedFileIds, activeFiles, activeFileIndex]);
// Reset active tab if it's out of bounds
useEffect(() => {
if (activeFileIndex >= activeFiles.length && activeFiles.length > 0) {
setActiveFileIndex(0);
}
}, [activeFiles.length, activeFileIndex]);
// Determine which file to display
const currentFile = React.useMemo(() => {
if (previewFile) {
return previewFile;
} else if (activeFiles.length > 0) {
return activeFiles[0]; // Use first file for simplicity
return activeFiles[activeFileIndex] || activeFiles[0];
}
return null;
}, [previewFile, activeFiles]);
}, [previewFile, activeFiles, activeFileIndex]);
// Get file with URL for rendering
const fileWithUrl = useFileWithUrl(currentFile);
@@ -244,15 +271,6 @@ const EmbedPdfViewerContent = ({
</Center>
) : (
<>
{/* Tabs for multiple files */}
{activeFiles.length > 1 && !previewFile && (
<Box p="md" style={{ borderBottom: `1px solid ${theme.colors.gray[3]}` }}>
<Text size="sm" c="dimmed">
Multiple files loaded - showing first file for now
</Text>
</Box>
)}
{/* EmbedPDF Viewer */}
<Box style={{
position: 'relative',
@@ -317,6 +335,7 @@ const EmbedPdfViewerContent = ({
<ThumbnailSidebar
visible={isThumbnailSidebarVisible}
onToggle={toggleThumbnailSidebar}
activeFileIndex={activeFileIndex}
/>
{/* Navigation Warning Modal */}

View File

@@ -18,7 +18,6 @@ import { SearchPluginPackage } from '@embedpdf/plugin-search/react';
import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/react';
import { RotatePluginPackage, Rotate } from '@embedpdf/plugin-rotate/react';
import { ExportPluginPackage } from '@embedpdf/plugin-export/react';
import { Rotation } from '@embedpdf/models';
// Import annotation plugins
import { HistoryPluginPackage } from '@embedpdf/plugin-history/react';
@@ -67,6 +66,10 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
const plugins = useMemo(() => {
if (!pdfUrl) return [];
// Calculate 3.5rem in pixels dynamically based on root font size
const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
const viewportGap = rootFontSize * 3.5;
return [
createPluginRegistration(LoaderPluginPackage, {
loadingOptions: {
@@ -78,7 +81,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
},
}),
createPluginRegistration(ViewportPluginPackage, {
viewportGap: 10,
viewportGap,
}),
createPluginRegistration(ScrollPluginPackage, {
strategy: ScrollStrategy.Vertical,
@@ -134,9 +137,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
createPluginRegistration(ThumbnailPluginPackage),
// Register rotate plugin
createPluginRegistration(RotatePluginPackage, {
defaultRotation: Rotation.Degree0, // Start with no rotation
}),
createPluginRegistration(RotatePluginPackage),
// Register export plugin for downloading PDFs
createPluginRegistration(ExportPluginPackage, {
@@ -288,48 +289,50 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
}}
>
<Scroller
renderPage={({ width, height, pageIndex, scale, rotation }: { width: number; height: number; pageIndex: number; scale: number; rotation?: number }) => (
<Rotate pageSize={{ width, height }}>
<PagePointerProvider {...{ pageWidth: width, pageHeight: height, pageIndex, scale, rotation: rotation || 0 }}>
<div
style={{
width,
height,
position: 'relative',
userSelect: 'none',
WebkitUserSelect: 'none',
MozUserSelect: 'none',
msUserSelect: 'none',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
}}
draggable={false}
onDragStart={(e) => e.preventDefault()}
onDrop={(e) => e.preventDefault()}
onDragOver={(e) => e.preventDefault()}
>
{/* High-resolution tile layer */}
<TilingLayer pageIndex={pageIndex} scale={scale} />
renderPage={({ document, width, height, pageIndex, scale, rotation }) => {
return (
<Rotate key={document?.id} pageSize={{ width, height }}>
<PagePointerProvider pageIndex={pageIndex} pageWidth={width} pageHeight={height} scale={scale} rotation={rotation}>
<div
style={{
width,
height,
position: 'relative',
userSelect: 'none',
WebkitUserSelect: 'none',
MozUserSelect: 'none',
msUserSelect: 'none',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
}}
draggable={false}
onDragStart={(e) => e.preventDefault()}
onDrop={(e) => e.preventDefault()}
onDragOver={(e) => e.preventDefault()}
>
{/* High-resolution tile layer */}
<TilingLayer pageIndex={pageIndex} scale={scale} />
{/* Search highlight layer */}
<CustomSearchLayer pageIndex={pageIndex} scale={scale} />
{/* Search highlight layer */}
<CustomSearchLayer pageIndex={pageIndex} scale={scale} />
{/* Selection layer for text interaction */}
<SelectionLayer pageIndex={pageIndex} scale={scale} />
{/* Annotation layer for signatures (only when enabled) */}
{enableAnnotations && (
<AnnotationLayer
pageIndex={pageIndex}
scale={scale}
pageWidth={width}
pageHeight={height}
rotation={rotation || 0}
selectionOutlineColor="#007ACC"
/>
)}
</div>
</PagePointerProvider>
</Rotate>
)}
{/* Selection layer for text interaction */}
<SelectionLayer pageIndex={pageIndex} scale={scale} />
{/* Annotation layer for signatures (only when enabled) */}
{enableAnnotations && (
<AnnotationLayer
pageIndex={pageIndex}
scale={scale}
pageWidth={width}
pageHeight={height}
rotation={rotation}
selectionOutlineColor="#007ACC"
/>
)}
</div>
</PagePointerProvider>
</Rotate>
);
}}
/>
</Viewport>
</GlobalPointerProvider>

View File

@@ -64,6 +64,10 @@ export function LocalEmbedPDFWithAnnotations({
const plugins = useMemo(() => {
if (!pdfUrl) return [];
// Calculate 3.5rem in pixels dynamically based on root font size
const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
const viewportGap = rootFontSize * 3.5;
return [
createPluginRegistration(LoaderPluginPackage, {
loadingOptions: {
@@ -75,7 +79,7 @@ export function LocalEmbedPDFWithAnnotations({
},
}),
createPluginRegistration(ViewportPluginPackage, {
viewportGap: 10,
viewportGap,
}),
createPluginRegistration(ScrollPluginPackage, {
strategy: ScrollStrategy.Vertical,

View File

@@ -5,15 +5,27 @@ import { useViewer } from '../../contexts/ViewerContext';
interface ThumbnailSidebarProps {
visible: boolean;
onToggle: () => void;
activeFileIndex?: number;
}
export function ThumbnailSidebar({ visible, onToggle: _onToggle }: ThumbnailSidebarProps) {
export function ThumbnailSidebar({ visible, onToggle: _onToggle, activeFileIndex }: ThumbnailSidebarProps) {
const { getScrollState, scrollActions, getThumbnailAPI } = useViewer();
const [thumbnails, setThumbnails] = useState<{ [key: number]: string }>({});
const scrollState = getScrollState();
const thumbnailAPI = getThumbnailAPI();
// Clear thumbnails when active file changes
useEffect(() => {
// Revoke old blob URLs to prevent memory leaks
Object.values(thumbnails).forEach((thumbUrl) => {
if (typeof thumbUrl === 'string' && thumbUrl.startsWith('blob:')) {
URL.revokeObjectURL(thumbUrl);
}
});
setThumbnails({});
}, [activeFileIndex]);
// Clear thumbnails when sidebar closes and revoke blob URLs to prevent memory leaks
useEffect(() => {
if (!visible) {

View File

@@ -5,6 +5,8 @@ export interface ViewerProps {
setSidebarsVisible: (v: boolean) => void;
onClose?: () => void;
previewFile?: File | null;
activeFileIndex?: number;
setActiveFileIndex?: (index: number) => void;
}
const Viewer = (props: ViewerProps) => {

View File

@@ -132,6 +132,10 @@ interface ViewerContextType {
setAnnotationMode: (enabled: boolean) => void;
toggleAnnotationMode: () => void;
// Active file index for multi-file viewing
activeFileIndex: number;
setActiveFileIndex: (index: number) => void;
// State getters - read current state from bridges
getScrollState: () => ScrollState;
getZoomState: () => ZoomState;
@@ -219,6 +223,7 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
const [isAnnotationsVisible, setIsAnnotationsVisible] = useState(true);
const [isAnnotationMode, setIsAnnotationModeState] = useState(false);
const [activeFileIndex, setActiveFileIndex] = useState(0);
// Get current navigation state to check if we're in sign mode
useNavigation();
@@ -577,6 +582,10 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
setAnnotationMode,
toggleAnnotationMode,
// Active file index
activeFileIndex,
setActiveFileIndex,
// State getters
getScrollState,
getZoomState,

View File

@@ -52,3 +52,12 @@ code {
color: var(--mantine-color-blue-8);
text-decoration: underline;
}
/* Viewer file tabs */
.viewer-file-tab {
justify-content: flex-start;
}
.viewer-file-tab[data-active] {
background-color: rgba(147, 197, 253, 0.5);
}

View File

@@ -17,7 +17,7 @@ const Sign = (props: BaseToolProps) => {
const { setWorkbench } = useNavigation();
const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode, updateDrawSettings, undo, redo, signatureApiRef, getImageData, setSignaturesApplied } = useSignature();
const { consumeFiles, selectors } = useFileContext();
const { exportActions, getScrollState } = useViewer();
const { exportActions, getScrollState, activeFileIndex, setActiveFileIndex } = useViewer();
const { setHasUnsavedChanges, unregisterUnsavedChangesChecker } = useNavigation();
// Track which signature mode was active for reactivation after save
@@ -75,19 +75,11 @@ const Sign = (props: BaseToolProps) => {
unregisterUnsavedChangesChecker();
setHasUnsavedChanges(false);
// Get the original file
let originalFile = null;
if (base.selectedFiles.length > 0) {
originalFile = base.selectedFiles[0];
} else {
const allFileIds = selectors.getAllFileIds();
if (allFileIds.length > 0) {
const stirlingFile = selectors.getFile(allFileIds[0]);
if (stirlingFile) {
originalFile = stirlingFile;
}
}
}
// Get the original file from FileContext using activeFileIndex
// The viewer displays files from FileContext, not from base.selectedFiles
const allFiles = selectors.getFiles();
const fileIndex = activeFileIndex < allFiles.length ? activeFileIndex : 0;
const originalFile = allFiles[fileIndex];
if (!originalFile) {
console.error('No file available to replace');
@@ -101,7 +93,8 @@ const Sign = (props: BaseToolProps) => {
exportActions,
selectors,
originalFile,
getScrollState
getScrollState,
activeFileIndex
});
if (flattenResult) {
@@ -112,6 +105,10 @@ const Sign = (props: BaseToolProps) => {
[flattenResult.outputStub]
);
// According to FileReducer.processFileSwap, new files are inserted at the beginning
// So the new file will be at index 0
setActiveFileIndex(0);
// Mark signatures as applied
setSignaturesApplied(true);
@@ -125,7 +122,7 @@ const Sign = (props: BaseToolProps) => {
} catch (error) {
console.error('Error saving signed document:', error);
}
}, [exportActions, base.selectedFiles, selectors, consumeFiles, signatureApiRef, getImageData, setWorkbench, activateDrawMode, setSignaturesApplied, getScrollState, handleDeactivateSignature, setHasUnsavedChanges, unregisterUnsavedChangesChecker]);
}, [exportActions, base.selectedFiles, selectors, consumeFiles, signatureApiRef, getImageData, setWorkbench, activateDrawMode, setSignaturesApplied, getScrollState, handleDeactivateSignature, setHasUnsavedChanges, unregisterUnsavedChangesChecker, activeFileIndex, setActiveFileIndex]);
const getSteps = () => {
const steps = [];

View File

@@ -19,6 +19,7 @@ interface SignatureFlatteningOptions {
selectors: MinimalFileContextSelectors;
originalFile?: StirlingFile;
getScrollState: () => { currentPage: number; totalPages: number };
activeFileIndex?: number;
}
export interface SignatureFlatteningResult {
@@ -28,7 +29,7 @@ export interface SignatureFlatteningResult {
}
export async function flattenSignatures(options: SignatureFlatteningOptions): Promise<SignatureFlatteningResult | null> {
const { signatureApiRef, getImageData, exportActions, selectors, originalFile, getScrollState } = options;
const { signatureApiRef, getImageData, exportActions, selectors, originalFile, getScrollState, activeFileIndex } = options;
try {
// Step 1: Extract all annotations from EmbedPDF before export
@@ -104,10 +105,12 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
if (!currentFile) {
const allFileIds = selectors.getAllFileIds();
if (allFileIds.length > 0) {
const fileStub = selectors.getStirlingFileStub(allFileIds[0]);
const fileObject = selectors.getFile(allFileIds[0]);
// Use activeFileIndex if provided, otherwise default to 0
const fileIndex = activeFileIndex !== undefined && activeFileIndex < allFileIds.length ? activeFileIndex : 0;
const fileStub = selectors.getStirlingFileStub(allFileIds[fileIndex]);
const fileObject = selectors.getFile(allFileIds[fileIndex]);
if (fileStub && fileObject) {
currentFile = createStirlingFile(fileObject, allFileIds[0] as FileId);
currentFile = createStirlingFile(fileObject, allFileIds[fileIndex] as FileId);
}
}
}