From d3494e32873ff9fbf64607e4e1e881dff5ccba35 Mon Sep 17 00:00:00 2001 From: Reece Browne <74901996+reecebrowne@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:15:32 +0000 Subject: [PATCH] Fix export (#5782) --- .../src/core/components/shared/RightRail.tsx | 68 ++++++++++--------- .../core/components/viewer/LocalEmbedPDF.tsx | 4 +- .../src/core/contexts/viewer/viewerActions.ts | 7 -- .../src/core/contexts/viewer/viewerBridges.ts | 1 - frontend/src/core/hooks/useFileWithUrl.ts | 24 ++++--- 5 files changed, 51 insertions(+), 53 deletions(-) diff --git a/frontend/src/core/components/shared/RightRail.tsx b/frontend/src/core/components/shared/RightRail.tsx index 7a6f78fed..11ecbb4a9 100644 --- a/frontend/src/core/components/shared/RightRail.tsx +++ b/frontend/src/core/components/shared/RightRail.tsx @@ -4,6 +4,7 @@ import '@app/components/shared/rightRail/RightRail.css'; import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; import { useRightRail } from '@app/contexts/RightRailContext'; import { useFileState, useFileSelection, useFileActions } from '@app/contexts/FileContext'; +import { isStirlingFile } from '@app/types/fileContext'; import { useNavigationState } from '@app/contexts/NavigationContext'; import { useTranslation } from 'react-i18next'; 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 { Tooltip } from '@app/components/shared/Tooltip'; import { ViewerContext } from '@app/contexts/ViewerContext'; -import { useSignature } from '@app/contexts/SignatureContext'; import LocalIcon from '@app/components/shared/LocalIcon'; import { RightRailFooterExtensions } from '@app/components/rightRail/RightRailFooterExtensions'; import DarkModeIcon from '@mui/icons-material/DarkMode'; @@ -61,12 +61,9 @@ export default function RightRail() { const { selectors } = useFileState(); const { selectedFiles, selectedFileIds } = useFileSelection(); const { actions: fileActions } = useFileActions(); - const { signaturesApplied } = useSignature(); - const activeFiles = selectors.getFiles(); const pageEditorTotalPages = pageEditorFunctions?.totalPages ?? 0; const pageEditorSelectedCount = pageEditorFunctions?.selectedPageIds?.length ?? 0; - const exportState = viewerContext?.getExportState?.(); const totalItems = useMemo(() => { if (currentView === 'pageEditor') return pageEditorTotalPages; @@ -139,11 +136,26 @@ export default function RightRail() { const handleExportAll = useCallback(async () => { if (currentView === 'viewer') { - if (!signaturesApplied) { - alert('You have unapplied signatures. Please use "Apply Signatures" first before exporting.'); - return; + const buffer = await viewerContext?.exportActions?.saveAsCopy?.(); + if (!buffer) 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; } @@ -153,30 +165,25 @@ export default function RightRail() { } const filesToExport = selectedFiles.length > 0 ? selectedFiles : activeFiles; - const stubsToExport = selectedFiles.length > 0 - ? selectors.getSelectedStirlingFileStubs() - : selectors.getStirlingFileStubs(); if (filesToExport.length > 0) { - for (let i = 0; i < filesToExport.length; i++) { - const file = filesToExport[i]; - const stub = stubsToExport[i]; - console.log('[RightRail] Exporting file:', { fileName: file.name, stubId: stub?.id, localFilePath: stub?.localFilePath, isDirty: stub?.isDirty }); - const result = await downloadFile({ - data: file, - filename: file.name, - 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 + for (const file of filesToExport) { + const stub = isStirlingFile(file) ? selectors.getStirlingFileStub(file.fileId) : undefined; + try { + const result = await downloadFile({ + data: file, + filename: file.name, + localPath: stub?.localFilePath }); - } else { - console.log('[RightRail] Skipping clean mark:', { savedPath: result.savedPath, isDirty: stub?.isDirty }); + if (result.cancelled) continue; + 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, pageEditorFunctions, viewerContext, - signaturesApplied, selectors, fileActions, ]); @@ -261,7 +267,7 @@ export default function RightRail() { onClick={handleExportAll} disabled={ disableForFullscreen || - (currentView === 'viewer' ? !exportState?.canExport : totalItems === 0 || allButtonsDisabled) + (currentView !== 'viewer' && (totalItems === 0 || allButtonsDisabled)) } > diff --git a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx index 83a7bd1dd..6a8a1de6a 100644 --- a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx +++ b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx @@ -59,8 +59,6 @@ import { ActiveDocumentProvider } from '@app/components/viewer/ActiveDocumentCon import { absoluteWithBasePath } from '@app/constants/app'; import { FormFieldOverlay } from '@app/tools/formFill/FormFieldOverlay'; -const DOCUMENT_NAME = 'stirling-pdf-viewer'; - interface LocalEmbedPDFProps { file?: File | Blob; url?: string | null; @@ -118,7 +116,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false, createPluginRegistration(DocumentManagerPluginPackage, { initialDocuments: [{ url: pdfUrl, - name: DOCUMENT_NAME, + name: exportFileName, }], }), createPluginRegistration(ViewportPluginPackage, { diff --git a/frontend/src/core/contexts/viewer/viewerActions.ts b/frontend/src/core/contexts/viewer/viewerActions.ts index 6358f6f68..6eebfb2f4 100644 --- a/frontend/src/core/contexts/viewer/viewerActions.ts +++ b/frontend/src/core/contexts/viewer/viewerActions.ts @@ -56,7 +56,6 @@ export interface SearchActions { } export interface ExportActions { - download: () => void; saveAsCopy: () => Promise; } @@ -336,12 +335,6 @@ export function createViewerActions({ }; const exportActions: ExportActions = { - download: () => { - const api = registry.current.export?.api; - if (api?.download) { - api.download(); - } - }, saveAsCopy: async () => { const api = registry.current.export?.api; if (api?.saveAsCopy) { diff --git a/frontend/src/core/contexts/viewer/viewerBridges.ts b/frontend/src/core/contexts/viewer/viewerBridges.ts index 30f465cfd..93e02ee5c 100644 --- a/frontend/src/core/contexts/viewer/viewerBridges.ts +++ b/frontend/src/core/contexts/viewer/viewerBridges.ts @@ -92,7 +92,6 @@ export interface ThumbnailAPIWrapper { } export interface ExportAPIWrapper { - download: () => void; saveAsCopy: () => { toPromise: () => Promise }; } diff --git a/frontend/src/core/hooks/useFileWithUrl.ts b/frontend/src/core/hooks/useFileWithUrl.ts index ce0d5945e..6c331e696 100644 --- a/frontend/src/core/hooks/useFileWithUrl.ts +++ b/frontend/src/core/hooks/useFileWithUrl.ts @@ -1,12 +1,12 @@ -import { useMemo } from 'react'; +import { useMemo, useEffect } from 'react'; import { isFileObject } from '@app/types/fileContext'; /** * 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 { - return useMemo(() => { + const result = useMemo(() => { if (!file) return null; // 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 { const url = URL.createObjectURL(file); - - // Return object with cleanup function - const result = { file, url }; - - // Store cleanup function for later use - (result as any)._cleanup = () => URL.revokeObjectURL(url); - - return result; + return { file, url }; } catch (error) { console.error('useFileWithUrl: Failed to create object URL:', error, file); return null; } }, [file]); + + useEffect(() => { + const url = result?.url; + return () => { + if (url) URL.revokeObjectURL(url); + }; + }, [result]); + + return result; } /**