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;
}
/**