mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 19:10:47 +02:00
Undo Button -> review state. Result files added to recents (#4337)
Produced PDFs go into recent files Undo button added to review state Undo causes undoConsume which replaces result files with source files. Removes result files from recent files too --------- Co-authored-by: Connor Yoh <[email protected]>
This commit is contained in:
@@ -21,6 +21,7 @@ interface BaseToolReturn<TParams> {
|
||||
handleExecute: () => Promise<void>;
|
||||
handleThumbnailClick: (file: File) => void;
|
||||
handleSettingsReset: () => void;
|
||||
handleUndo: () => Promise<void>;
|
||||
|
||||
// Standard computed state
|
||||
hasFiles: boolean;
|
||||
@@ -88,6 +89,11 @@ export function useBaseTool<TParams>(
|
||||
onPreviewFile?.(null);
|
||||
}, [operation, onPreviewFile]);
|
||||
|
||||
const handleUndo = useCallback(async () => {
|
||||
await operation.undoOperation();
|
||||
onPreviewFile?.(null);
|
||||
}, [operation, onPreviewFile]);
|
||||
|
||||
// Standard computed state
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = operation.files.length > 0 || operation.downloadUrl !== null;
|
||||
@@ -109,6 +115,7 @@ export function useBaseTool<TParams>(
|
||||
handleExecute,
|
||||
handleThumbnailClick,
|
||||
handleSettingsReset,
|
||||
handleUndo,
|
||||
|
||||
// State
|
||||
hasFiles,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useRef, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFileContext } from '../../../contexts/FileContext';
|
||||
@@ -9,6 +9,7 @@ import { extractErrorMessage } from '../../../utils/toolErrorHandler';
|
||||
import { createOperation } from '../../../utils/toolOperationTracker';
|
||||
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
|
||||
import { FileId } from '../../../types/file';
|
||||
import { FileRecord } from '../../../types/fileContext';
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
export type { ProcessingProgress, ResponseHandler };
|
||||
@@ -107,6 +108,7 @@ export interface ToolOperationHook<TParams = void> {
|
||||
resetResults: () => void;
|
||||
clearError: () => void;
|
||||
cancelOperation: () => void;
|
||||
undoOperation: () => Promise<void>;
|
||||
}
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
@@ -128,13 +130,20 @@ export const useToolOperation = <TParams>(
|
||||
config: ToolOperationConfig<TParams>
|
||||
): ToolOperationHook<TParams> => {
|
||||
const { t } = useTranslation();
|
||||
const { recordOperation, markOperationApplied, markOperationFailed, addFiles, consumeFiles, findFileId } = useFileContext();
|
||||
const { recordOperation, markOperationApplied, markOperationFailed, addFiles, consumeFiles, undoConsumeFiles, findFileId, actions: fileActions, selectors } = useFileContext();
|
||||
|
||||
// Composed hooks
|
||||
const { state, actions } = useToolState();
|
||||
const { processFiles, cancelOperation: cancelApiCalls } = useToolApiCalls<TParams>();
|
||||
const { generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles } = useToolResources();
|
||||
|
||||
// Track last operation for undo functionality
|
||||
const lastOperationRef = useRef<{
|
||||
inputFiles: File[];
|
||||
inputFileRecords: FileRecord[];
|
||||
outputFileIds: FileId[];
|
||||
} | null>(null);
|
||||
|
||||
const executeOperation = useCallback(async (
|
||||
params: TParams,
|
||||
selectedFiles: File[]
|
||||
@@ -232,8 +241,33 @@ export const useToolOperation = <TParams>(
|
||||
actions.setDownloadInfo(downloadInfo.url, downloadInfo.filename);
|
||||
|
||||
// Replace input files with processed files (consumeFiles handles pinning)
|
||||
const inputFileIds = validFiles.map(file => findFileId(file)).filter(Boolean) as FileId[];
|
||||
await consumeFiles(inputFileIds, processedFiles);
|
||||
const inputFileIds: FileId[] = [];
|
||||
const inputFileRecords: FileRecord[] = [];
|
||||
|
||||
// Build parallel arrays of IDs and records for undo tracking
|
||||
for (const file of validFiles) {
|
||||
const fileId = findFileId(file);
|
||||
if (fileId) {
|
||||
const record = selectors.getFileRecord(fileId);
|
||||
if (record) {
|
||||
inputFileIds.push(fileId);
|
||||
inputFileRecords.push(record);
|
||||
} else {
|
||||
console.warn(`No file record found for file: ${file.name}`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`No file ID found for file: ${file.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
const outputFileIds = await consumeFiles(inputFileIds, processedFiles);
|
||||
|
||||
// Store operation data for undo (only store what we need to avoid memory bloat)
|
||||
lastOperationRef.current = {
|
||||
inputFiles: validFiles, // Keep original File objects for undo
|
||||
inputFileRecords: inputFileRecords.map(record => ({ ...record })), // Deep copy to avoid reference issues
|
||||
outputFileIds
|
||||
};
|
||||
|
||||
markOperationApplied(fileId, operationId);
|
||||
}
|
||||
@@ -259,8 +293,65 @@ export const useToolOperation = <TParams>(
|
||||
const resetResults = useCallback(() => {
|
||||
cleanupBlobUrls();
|
||||
actions.resetResults();
|
||||
// Clear undo data when results are reset to prevent memory leaks
|
||||
lastOperationRef.current = null;
|
||||
}, [cleanupBlobUrls, actions]);
|
||||
|
||||
// Cleanup on unmount to prevent memory leaks
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
lastOperationRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const undoOperation = useCallback(async () => {
|
||||
if (!lastOperationRef.current) {
|
||||
actions.setError(t('noOperationToUndo', 'No operation to undo'));
|
||||
return;
|
||||
}
|
||||
|
||||
const { inputFiles, inputFileRecords, outputFileIds } = lastOperationRef.current;
|
||||
|
||||
// Validate that we have data to undo
|
||||
if (inputFiles.length === 0 || inputFileRecords.length === 0) {
|
||||
actions.setError(t('invalidUndoData', 'Cannot undo: invalid operation data'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (outputFileIds.length === 0) {
|
||||
actions.setError(t('noFilesToUndo', 'Cannot undo: no files were processed in the last operation'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Undo the consume operation
|
||||
await undoConsumeFiles(inputFiles, inputFileRecords, outputFileIds);
|
||||
|
||||
// Clear results and operation tracking
|
||||
resetResults();
|
||||
lastOperationRef.current = null;
|
||||
|
||||
// Show success message
|
||||
actions.setStatus(t('undoSuccess', 'Operation undone successfully'));
|
||||
|
||||
} catch (error: any) {
|
||||
let errorMessage = extractErrorMessage(error);
|
||||
|
||||
// Provide more specific error messages based on error type
|
||||
if (error.message?.includes('Mismatch between input files')) {
|
||||
errorMessage = t('undoDataMismatch', 'Cannot undo: operation data is corrupted');
|
||||
} else if (error.message?.includes('IndexedDB')) {
|
||||
errorMessage = t('undoStorageError', 'Undo completed but some files could not be saved to storage');
|
||||
} else if (error.name === 'QuotaExceededError') {
|
||||
errorMessage = t('undoQuotaError', 'Cannot undo: insufficient storage space');
|
||||
}
|
||||
|
||||
actions.setError(`${t('undoFailed', 'Failed to undo operation')}: ${errorMessage}`);
|
||||
|
||||
// Don't clear the operation data if undo failed - user might want to try again
|
||||
}
|
||||
}, [undoConsumeFiles, resetResults, actions, t]);
|
||||
|
||||
return {
|
||||
// State
|
||||
files: state.files,
|
||||
@@ -277,6 +368,7 @@ export const useToolOperation = <TParams>(
|
||||
executeOperation,
|
||||
resetResults,
|
||||
clearError: actions.clearError,
|
||||
cancelOperation
|
||||
cancelOperation,
|
||||
undoOperation
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user