Fix export (#5782)

This commit is contained in:
Reece Browne
2026-02-23 22:15:32 +00:00
committed by GitHub
parent 9b0610b2cc
commit d3494e3287
5 changed files with 51 additions and 53 deletions
@@ -4,6 +4,7 @@ import '@app/components/shared/rightRail/RightRail.css';
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
import { useRightRail } from '@app/contexts/RightRailContext'; import { useRightRail } from '@app/contexts/RightRailContext';
import { useFileState, useFileSelection, useFileActions } from '@app/contexts/FileContext'; import { useFileState, useFileSelection, useFileActions } from '@app/contexts/FileContext';
import { isStirlingFile } from '@app/types/fileContext';
import { useNavigationState } from '@app/contexts/NavigationContext'; import { useNavigationState } from '@app/contexts/NavigationContext';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology';
@@ -13,7 +14,6 @@ import LanguageSelector from '@app/components/shared/LanguageSelector';
import { useRainbowThemeContext } from '@app/components/shared/RainbowThemeProvider'; import { useRainbowThemeContext } from '@app/components/shared/RainbowThemeProvider';
import { Tooltip } from '@app/components/shared/Tooltip'; import { Tooltip } from '@app/components/shared/Tooltip';
import { ViewerContext } from '@app/contexts/ViewerContext'; import { ViewerContext } from '@app/contexts/ViewerContext';
import { useSignature } from '@app/contexts/SignatureContext';
import LocalIcon from '@app/components/shared/LocalIcon'; import LocalIcon from '@app/components/shared/LocalIcon';
import { RightRailFooterExtensions } from '@app/components/rightRail/RightRailFooterExtensions'; import { RightRailFooterExtensions } from '@app/components/rightRail/RightRailFooterExtensions';
import DarkModeIcon from '@mui/icons-material/DarkMode'; import DarkModeIcon from '@mui/icons-material/DarkMode';
@@ -61,12 +61,9 @@ export default function RightRail() {
const { selectors } = useFileState(); const { selectors } = useFileState();
const { selectedFiles, selectedFileIds } = useFileSelection(); const { selectedFiles, selectedFileIds } = useFileSelection();
const { actions: fileActions } = useFileActions(); const { actions: fileActions } = useFileActions();
const { signaturesApplied } = useSignature();
const activeFiles = selectors.getFiles(); const activeFiles = selectors.getFiles();
const pageEditorTotalPages = pageEditorFunctions?.totalPages ?? 0; const pageEditorTotalPages = pageEditorFunctions?.totalPages ?? 0;
const pageEditorSelectedCount = pageEditorFunctions?.selectedPageIds?.length ?? 0; const pageEditorSelectedCount = pageEditorFunctions?.selectedPageIds?.length ?? 0;
const exportState = viewerContext?.getExportState?.();
const totalItems = useMemo(() => { const totalItems = useMemo(() => {
if (currentView === 'pageEditor') return pageEditorTotalPages; if (currentView === 'pageEditor') return pageEditorTotalPages;
@@ -139,11 +136,26 @@ export default function RightRail() {
const handleExportAll = useCallback(async () => { const handleExportAll = useCallback(async () => {
if (currentView === 'viewer') { if (currentView === 'viewer') {
if (!signaturesApplied) { const buffer = await viewerContext?.exportActions?.saveAsCopy?.();
alert('You have unapplied signatures. Please use "Apply Signatures" first before exporting.'); if (!buffer) return;
return; const fileToExport = selectedFiles.length > 0 ? selectedFiles[0] : activeFiles[0];
if (!fileToExport) return;
const stub = isStirlingFile(fileToExport) ? selectors.getStirlingFileStub(fileToExport.fileId) : undefined;
try {
const result = await downloadFile({
data: new Blob([buffer], { type: 'application/pdf' }),
filename: fileToExport.name,
localPath: stub?.localFilePath,
});
if (!result.cancelled && stub && result.savedPath) {
fileActions.updateStirlingFileStub(stub.id, {
localFilePath: stub.localFilePath ?? result.savedPath,
isDirty: false,
});
}
} catch (error) {
console.error('[RightRail] Failed to export viewer file:', error);
} }
viewerContext?.exportActions?.download?.();
return; return;
} }
@@ -153,30 +165,25 @@ export default function RightRail() {
} }
const filesToExport = selectedFiles.length > 0 ? selectedFiles : activeFiles; const filesToExport = selectedFiles.length > 0 ? selectedFiles : activeFiles;
const stubsToExport = selectedFiles.length > 0
? selectors.getSelectedStirlingFileStubs()
: selectors.getStirlingFileStubs();
if (filesToExport.length > 0) { if (filesToExport.length > 0) {
for (let i = 0; i < filesToExport.length; i++) { for (const file of filesToExport) {
const file = filesToExport[i]; const stub = isStirlingFile(file) ? selectors.getStirlingFileStub(file.fileId) : undefined;
const stub = stubsToExport[i]; try {
console.log('[RightRail] Exporting file:', { fileName: file.name, stubId: stub?.id, localFilePath: stub?.localFilePath, isDirty: stub?.isDirty }); const result = await downloadFile({
const result = await downloadFile({ data: file,
data: file, filename: file.name,
filename: file.name, localPath: stub?.localFilePath
localPath: stub?.localFilePath
});
console.log('[RightRail] Export complete, checking dirty state:', { localFilePath: stub?.localFilePath, isDirty: stub?.isDirty, savedPath: result.savedPath });
// Mark file as clean after successful save to disk
if (stub && result.savedPath) {
console.log('[RightRail] Marking file as clean:', stub.id);
fileActions.updateStirlingFileStub(stub.id, {
localFilePath: stub.localFilePath ?? result.savedPath,
isDirty: false
}); });
} else { if (result.cancelled) continue;
console.log('[RightRail] Skipping clean mark:', { savedPath: result.savedPath, isDirty: stub?.isDirty }); if (stub && result.savedPath) {
fileActions.updateStirlingFileStub(stub.id, {
localFilePath: stub.localFilePath ?? result.savedPath,
isDirty: false
});
}
} catch (error) {
console.error('[RightRail] Failed to export file:', file.name, error);
} }
} }
} }
@@ -186,7 +193,6 @@ export default function RightRail() {
activeFiles, activeFiles,
pageEditorFunctions, pageEditorFunctions,
viewerContext, viewerContext,
signaturesApplied,
selectors, selectors,
fileActions, fileActions,
]); ]);
@@ -261,7 +267,7 @@ export default function RightRail() {
onClick={handleExportAll} onClick={handleExportAll}
disabled={ disabled={
disableForFullscreen || disableForFullscreen ||
(currentView === 'viewer' ? !exportState?.canExport : totalItems === 0 || allButtonsDisabled) (currentView !== 'viewer' && (totalItems === 0 || allButtonsDisabled))
} }
> >
<LocalIcon icon={icons.downloadIconName} width="1.5rem" height="1.5rem" /> <LocalIcon icon={icons.downloadIconName} width="1.5rem" height="1.5rem" />
@@ -59,8 +59,6 @@ import { ActiveDocumentProvider } from '@app/components/viewer/ActiveDocumentCon
import { absoluteWithBasePath } from '@app/constants/app'; import { absoluteWithBasePath } from '@app/constants/app';
import { FormFieldOverlay } from '@app/tools/formFill/FormFieldOverlay'; import { FormFieldOverlay } from '@app/tools/formFill/FormFieldOverlay';
const DOCUMENT_NAME = 'stirling-pdf-viewer';
interface LocalEmbedPDFProps { interface LocalEmbedPDFProps {
file?: File | Blob; file?: File | Blob;
url?: string | null; url?: string | null;
@@ -118,7 +116,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
createPluginRegistration(DocumentManagerPluginPackage, { createPluginRegistration(DocumentManagerPluginPackage, {
initialDocuments: [{ initialDocuments: [{
url: pdfUrl, url: pdfUrl,
name: DOCUMENT_NAME, name: exportFileName,
}], }],
}), }),
createPluginRegistration(ViewportPluginPackage, { createPluginRegistration(ViewportPluginPackage, {
@@ -56,7 +56,6 @@ export interface SearchActions {
} }
export interface ExportActions { export interface ExportActions {
download: () => void;
saveAsCopy: () => Promise<ArrayBuffer | null>; saveAsCopy: () => Promise<ArrayBuffer | null>;
} }
@@ -336,12 +335,6 @@ export function createViewerActions({
}; };
const exportActions: ExportActions = { const exportActions: ExportActions = {
download: () => {
const api = registry.current.export?.api;
if (api?.download) {
api.download();
}
},
saveAsCopy: async () => { saveAsCopy: async () => {
const api = registry.current.export?.api; const api = registry.current.export?.api;
if (api?.saveAsCopy) { if (api?.saveAsCopy) {
@@ -92,7 +92,6 @@ export interface ThumbnailAPIWrapper {
} }
export interface ExportAPIWrapper { export interface ExportAPIWrapper {
download: () => void;
saveAsCopy: () => { toPromise: () => Promise<ArrayBuffer> }; saveAsCopy: () => { toPromise: () => Promise<ArrayBuffer> };
} }
+13 -11
View File
@@ -1,12 +1,12 @@
import { useMemo } from 'react'; import { useMemo, useEffect } from 'react';
import { isFileObject } from '@app/types/fileContext'; import { isFileObject } from '@app/types/fileContext';
/** /**
* Hook to convert a File object to { file: File; url: string } format * Hook to convert a File object to { file: File; url: string } format
* Creates blob URL on-demand and handles cleanup * Creates blob URL on-demand and revokes it when file changes or component unmounts.
*/ */
export function useFileWithUrl(file: File | Blob | null): { file: File | Blob; url: string } | null { export function useFileWithUrl(file: File | Blob | null): { file: File | Blob; url: string } | null {
return useMemo(() => { const result = useMemo(() => {
if (!file) return null; if (!file) return null;
// Validate that file is a proper File, StirlingFile, or Blob object // Validate that file is a proper File, StirlingFile, or Blob object
@@ -17,19 +17,21 @@ export function useFileWithUrl(file: File | Blob | null): { file: File | Blob; u
try { try {
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
return { file, url };
// Return object with cleanup function
const result = { file, url };
// Store cleanup function for later use
(result as any)._cleanup = () => URL.revokeObjectURL(url);
return result;
} catch (error) { } catch (error) {
console.error('useFileWithUrl: Failed to create object URL:', error, file); console.error('useFileWithUrl: Failed to create object URL:', error, file);
return null; return null;
} }
}, [file]); }, [file]);
useEffect(() => {
const url = result?.url;
return () => {
if (url) URL.revokeObjectURL(url);
};
}, [result]);
return result;
} }
/** /**