mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 01:19:24 +00:00
Merge
This commit is contained in:
parent
dad9f20879
commit
564b14e3e2
@ -13,7 +13,9 @@
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(node:*)",
|
||||
"Bash(npm run dev:*)",
|
||||
"Bash(sed:*)"
|
||||
"Bash(sed:*)",
|
||||
"Bash(npm run typecheck:*)",
|
||||
"Bash(git checkout:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"defaultMode": "acceptEdits"
|
||||
|
@ -41,6 +41,7 @@
|
||||
"prebuild": "npm run generate-icons",
|
||||
"build": "npx tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"generate-licenses": "node scripts/generate-licenses.js",
|
||||
"generate-icons": "node scripts/generate-icons.js",
|
||||
"generate-icons:verbose": "node scripts/generate-icons.js --verbose",
|
||||
|
@ -41,6 +41,9 @@
|
||||
"save": "Save",
|
||||
"saveToBrowser": "Save to Browser",
|
||||
"download": "Download",
|
||||
"undoOperationTooltip": "Click to undo the last operation and restore the original files",
|
||||
"undo": "Undo",
|
||||
"moreOptions": "More Options",
|
||||
"editYourNewFiles": "Edit your new file(s)",
|
||||
"close": "Close",
|
||||
"fileSelected": "Selected: {{filename}}",
|
||||
|
@ -350,9 +350,9 @@ const FileEditor = ({
|
||||
if (record) {
|
||||
// Set the file as selected in context and switch to viewer for preview
|
||||
setSelectedFiles([fileId as FileId]);
|
||||
navActions.setMode('viewer');
|
||||
navActions.setWorkbench('viewer');
|
||||
}
|
||||
}, [activeFileRecords, setSelectedFiles, navActions.setMode]);
|
||||
}, [activeFileRecords, setSelectedFiles, navActions.setWorkbench]);
|
||||
|
||||
const handleMergeFromHere = useCallback((fileId: string) => {
|
||||
const startIndex = activeFileRecords.findIndex(r => r.id === fileId);
|
||||
|
@ -1,26 +1,47 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Button, Stack, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import ErrorNotification from './ErrorNotification';
|
||||
import ResultsPreview from './ResultsPreview';
|
||||
import { SuggestedToolsSection } from './SuggestedToolsSection';
|
||||
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Button, Group, Stack } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import UndoIcon from "@mui/icons-material/Undo";
|
||||
import ErrorNotification from "./ErrorNotification";
|
||||
import ResultsPreview from "./ResultsPreview";
|
||||
import { SuggestedToolsSection } from "./SuggestedToolsSection";
|
||||
import { ToolOperationHook } from "../../../hooks/tools/shared/useToolOperation";
|
||||
import { Tooltip } from "../../shared/Tooltip";
|
||||
|
||||
export interface ReviewToolStepProps<TParams = unknown> {
|
||||
isVisible: boolean;
|
||||
operation: ToolOperationHook<TParams>;
|
||||
title?: string;
|
||||
onFileClick?: (file: File) => void;
|
||||
onUndo: () => void;
|
||||
}
|
||||
|
||||
function ReviewStepContent<TParams = unknown>({ operation, onFileClick }: { operation: ToolOperationHook<TParams>; onFileClick?: (file: File) => void }) {
|
||||
function ReviewStepContent<TParams = unknown>({
|
||||
operation,
|
||||
onFileClick,
|
||||
onUndo,
|
||||
}: {
|
||||
operation: ToolOperationHook<TParams>;
|
||||
onFileClick?: (file: File) => void;
|
||||
onUndo: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const stepRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const previewFiles = operation.files?.map((file, index) => ({
|
||||
const handleUndo = async () => {
|
||||
try {
|
||||
onUndo();
|
||||
} catch (error) {
|
||||
// Error is already handled by useToolOperation, just reset loading state
|
||||
console.error("Undo operation failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const previewFiles =
|
||||
operation.files?.map((file, index) => ({
|
||||
file,
|
||||
thumbnail: operation.thumbnails[index]
|
||||
thumbnail: operation.thumbnails[index],
|
||||
})) || [];
|
||||
|
||||
// Auto-scroll to bottom when content appears
|
||||
@ -31,7 +52,7 @@ function ReviewStepContent<TParams = unknown>({ operation, onFileClick }: { oper
|
||||
setTimeout(() => {
|
||||
scrollableContainer.scrollTo({
|
||||
top: scrollableContainer.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
behavior: "smooth",
|
||||
});
|
||||
}, 100); // Small delay to ensure content is rendered
|
||||
}
|
||||
@ -40,10 +61,7 @@ function ReviewStepContent<TParams = unknown>({ operation, onFileClick }: { oper
|
||||
|
||||
return (
|
||||
<Stack gap="sm" ref={stepRef}>
|
||||
<ErrorNotification
|
||||
error={operation.errorMessage}
|
||||
onClose={operation.clearError}
|
||||
/>
|
||||
<ErrorNotification error={operation.errorMessage} onClose={operation.clearError} />
|
||||
|
||||
{previewFiles.length > 0 && (
|
||||
<ResultsPreview
|
||||
@ -53,6 +71,17 @@ function ReviewStepContent<TParams = unknown>({ operation, onFileClick }: { oper
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tooltip content={t("undoOperationTooltip", "Click to undo the last operation and restore the original files")}>
|
||||
<Button
|
||||
leftSection={<UndoIcon />}
|
||||
variant="outline"
|
||||
color="var(--mantine-color-gray-6)"
|
||||
onClick={handleUndo}
|
||||
fullWidth
|
||||
>
|
||||
{t("undo", "Undo")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{operation.downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
@ -78,14 +107,13 @@ export function createReviewToolStep<TParams = unknown>(
|
||||
): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return createStep(t("review", "Review"), {
|
||||
return createStep(
|
||||
t("review", "Review"),
|
||||
{
|
||||
isVisible: props.isVisible,
|
||||
_excludeFromCount: true,
|
||||
_noPadding: true
|
||||
}, (
|
||||
<ReviewStepContent
|
||||
operation={props.operation}
|
||||
onFileClick={props.onFileClick}
|
||||
/>
|
||||
));
|
||||
_noPadding: true,
|
||||
},
|
||||
<ReviewStepContent operation={props.operation} onFileClick={props.onFileClick} onUndo={props.onUndo} />
|
||||
);
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ export interface ReviewStepConfig {
|
||||
operation: ToolOperationHook<any>;
|
||||
title: string;
|
||||
onFileClick?: (file: File) => void;
|
||||
onUndo: () => void;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
@ -106,7 +107,8 @@ export function createToolFlow(config: ToolFlowConfig) {
|
||||
isVisible: config.review.isVisible,
|
||||
operation: config.review.operation,
|
||||
title: config.review.title,
|
||||
onFileClick: config.review.onFileClick
|
||||
onFileClick: config.review.onFileClick,
|
||||
onUndo: config.review.onUndo
|
||||
})}
|
||||
</ToolStepProvider>
|
||||
</Stack>
|
||||
|
@ -28,7 +28,7 @@ import {
|
||||
// Import modular components
|
||||
import { fileContextReducer, initialFileContextState } from './file/FileReducer';
|
||||
import { createFileSelectors } from './file/fileSelectors';
|
||||
import { addFiles, consumeFiles, createFileActions } from './file/fileActions';
|
||||
import { addFiles, consumeFiles, undoConsumeFiles, createFileActions } from './file/fileActions';
|
||||
import { FileLifecycleManager } from './file/lifecycle';
|
||||
import { FileStateContext, FileActionsContext } from './file/contexts';
|
||||
import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext';
|
||||
@ -74,10 +74,21 @@ function FileContextInner({
|
||||
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });
|
||||
}, []);
|
||||
|
||||
const selectFiles = useCallback((addedFilesWithIds: Array<{ file: File; id: FileId; thumbnail?: string }>) => {
|
||||
const currentSelection = stateRef.current.ui.selectedFileIds;
|
||||
const newFileIds = addedFilesWithIds.map(({ id }) => id);
|
||||
dispatch({ type: 'SET_SELECTED_FILES', payload: { fileIds: [...currentSelection, ...newFileIds] } });
|
||||
}, []);
|
||||
|
||||
// File operations using unified addFiles helper with persistence
|
||||
const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string }): Promise<FileWithId[]> => {
|
||||
const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise<FileWithId[]> => {
|
||||
const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||
|
||||
// Auto-select the newly added files if requested
|
||||
if (options?.selectFiles && addedFilesWithIds.length > 0) {
|
||||
selectFiles(addedFilesWithIds);
|
||||
}
|
||||
|
||||
// Persist to IndexedDB if enabled
|
||||
if (indexedDB && enablePersistence && addedFilesWithIds.length > 0) {
|
||||
await Promise.all(addedFilesWithIds.map(async ({ file, id, thumbnail }) => {
|
||||
@ -97,8 +108,14 @@ function FileContextInner({
|
||||
return result.map(({ file, id }) => createFileWithId(file, id));
|
||||
}, []);
|
||||
|
||||
const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>): Promise<FileWithId[]> => {
|
||||
const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>, options?: { selectFiles?: boolean }): Promise<FileWithId[]> => {
|
||||
const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||
|
||||
// Auto-select the newly added files if requested
|
||||
if (options?.selectFiles && result.length > 0) {
|
||||
selectFiles(result);
|
||||
}
|
||||
|
||||
return result.map(({ file, id }) => createFileWithId(file, id));
|
||||
}, []);
|
||||
|
||||
@ -106,10 +123,13 @@ function FileContextInner({
|
||||
const baseActions = useMemo(() => createFileActions(dispatch), []);
|
||||
|
||||
// Helper functions for pinned files
|
||||
const consumeFilesWrapper = useCallback(async (inputFileIds: FileId[], outputFiles: File[]): Promise<FileWithId[]> => {
|
||||
const result = await consumeFiles(inputFileIds, outputFiles, stateRef, filesRef, dispatch);
|
||||
return result.map(({ file, id }) => createFileWithId(file, id));
|
||||
}, []);
|
||||
const consumeFilesWrapper = useCallback(async (inputFileIds: FileId[], outputFiles: File[]): Promise<FileId[]> => {
|
||||
return consumeFiles(inputFileIds, outputFiles, stateRef, filesRef, dispatch, indexedDB);
|
||||
}, [indexedDB]);
|
||||
|
||||
const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputFileRecords: FileRecord[], outputFileIds: FileId[]): Promise<void> => {
|
||||
return undoConsumeFiles(inputFiles, inputFileRecords, outputFileIds, stateRef, filesRef, dispatch, indexedDB);
|
||||
}, [indexedDB]);
|
||||
|
||||
const pinFileWrapper = useCallback((file: FileWithId) => {
|
||||
baseActions.pinFile(file.fileId);
|
||||
@ -170,6 +190,7 @@ function FileContextInner({
|
||||
pinFile: pinFileWrapper,
|
||||
unpinFile: unpinFileWrapper,
|
||||
consumeFiles: consumeFilesWrapper,
|
||||
undoConsumeFiles: undoConsumeFilesWrapper,
|
||||
setHasUnsavedChanges,
|
||||
trackBlobUrl: lifecycleManager.trackBlobUrl,
|
||||
cleanupFile: (fileId: string) => lifecycleManager.cleanupFile(fileId, stateRef),
|
||||
@ -183,6 +204,7 @@ function FileContextInner({
|
||||
lifecycleManager,
|
||||
setHasUnsavedChanges,
|
||||
consumeFilesWrapper,
|
||||
undoConsumeFilesWrapper,
|
||||
pinFileWrapper,
|
||||
unpinFileWrapper,
|
||||
indexedDB,
|
||||
|
@ -62,6 +62,7 @@ const initialState: NavigationState = {
|
||||
// Navigation context actions interface
|
||||
export interface NavigationContextActions {
|
||||
setMode: (mode: ModeType) => void;
|
||||
setWorkbench: (mode: ModeType) => void; // Alias for V2 compatibility
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
||||
showNavigationWarning: (show: boolean) => void;
|
||||
requestNavigation: (navigationFn: () => void) => void;
|
||||
@ -101,6 +102,10 @@ export const NavigationProvider: React.FC<{
|
||||
dispatch({ type: 'SET_MODE', payload: { mode } });
|
||||
}, []),
|
||||
|
||||
setWorkbench: useCallback((mode: ModeType) => {
|
||||
dispatch({ type: 'SET_MODE', payload: { mode } });
|
||||
}, []),
|
||||
|
||||
setHasUnsavedChanges: useCallback((hasChanges: boolean) => {
|
||||
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });
|
||||
}, []),
|
||||
|
@ -25,6 +25,47 @@ export const initialFileContextState: FileContextState = {
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function for consume/undo operations
|
||||
function processFileSwap(
|
||||
state: FileContextState,
|
||||
filesToRemove: FileId[],
|
||||
filesToAdd: FileRecord[]
|
||||
): FileContextState {
|
||||
// Only remove unpinned files
|
||||
const unpinnedRemoveIds = filesToRemove.filter(id => !state.pinnedFiles.has(id));
|
||||
const remainingIds = state.files.ids.filter(id => !unpinnedRemoveIds.includes(id));
|
||||
|
||||
// Remove unpinned files from state
|
||||
const newById = { ...state.files.byId };
|
||||
unpinnedRemoveIds.forEach(id => {
|
||||
delete newById[id];
|
||||
});
|
||||
|
||||
// Add new files
|
||||
const addedIds: FileId[] = [];
|
||||
filesToAdd.forEach(record => {
|
||||
if (!newById[record.id]) {
|
||||
addedIds.push(record.id);
|
||||
newById[record.id] = record;
|
||||
}
|
||||
});
|
||||
|
||||
// Clear selections that reference removed files
|
||||
const validSelectedFileIds = state.ui.selectedFileIds.filter(id => !unpinnedRemoveIds.includes(id));
|
||||
|
||||
return {
|
||||
...state,
|
||||
files: {
|
||||
ids: [...addedIds, ...remainingIds],
|
||||
byId: newById
|
||||
},
|
||||
ui: {
|
||||
...state.ui,
|
||||
selectedFileIds: validSelectedFileIds
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Pure reducer function
|
||||
export function fileContextReducer(state: FileContextState, action: FileContextAction): FileContextState {
|
||||
switch (action.type) {
|
||||
@ -193,40 +234,12 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
||||
|
||||
case 'CONSUME_FILES': {
|
||||
const { inputFileIds, outputFileRecords } = action.payload;
|
||||
|
||||
// Only remove unpinned input files
|
||||
const unpinnedInputIds = inputFileIds.filter(id => !state.pinnedFiles.has(id));
|
||||
const remainingIds = state.files.ids.filter(id => !unpinnedInputIds.includes(id));
|
||||
|
||||
// Remove unpinned files from state
|
||||
const newById = { ...state.files.byId };
|
||||
unpinnedInputIds.forEach(id => {
|
||||
delete newById[id];
|
||||
});
|
||||
|
||||
// Add output files
|
||||
const outputIds: FileId[] = [];
|
||||
outputFileRecords.forEach(record => {
|
||||
if (!newById[record.id]) {
|
||||
outputIds.push(record.id);
|
||||
newById[record.id] = record;
|
||||
return processFileSwap(state, inputFileIds, outputFileRecords);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear selections that reference removed files
|
||||
const validSelectedFileIds = state.ui.selectedFileIds.filter(id => !unpinnedInputIds.includes(id));
|
||||
|
||||
return {
|
||||
...state,
|
||||
files: {
|
||||
ids: [...remainingIds, ...outputIds],
|
||||
byId: newById
|
||||
},
|
||||
ui: {
|
||||
...state.ui,
|
||||
selectedFileIds: validSelectedFileIds
|
||||
}
|
||||
};
|
||||
case 'UNDO_CONSUME_FILES': {
|
||||
const { inputFileRecords, outputFileIds } = action.payload;
|
||||
return processFileSwap(state, outputFileIds, inputFileRecords);
|
||||
}
|
||||
|
||||
case 'RESET_CONTEXT': {
|
||||
|
@ -318,34 +318,28 @@ export async function addFiles(
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume files helper - replace unpinned input files with output files
|
||||
* Helper function to process files into records with thumbnails and metadata
|
||||
*/
|
||||
export async function consumeFiles(
|
||||
inputFileIds: FileId[],
|
||||
outputFiles: File[],
|
||||
stateRef: React.MutableRefObject<FileContextState>,
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||
dispatch: React.Dispatch<FileContextAction>
|
||||
): Promise<Array<{ file: File; id: FileId; thumbnail?: string }>> {
|
||||
if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputFiles.length} output files`);
|
||||
|
||||
// Process output files through the 'processed' path to generate thumbnails
|
||||
const processedOutputs: Array<{ file: File; id: FileId; thumbnail?: string; record: FileRecord }> = await Promise.all(
|
||||
outputFiles.map(async (file) => {
|
||||
async function processFilesIntoRecords(
|
||||
files: File[],
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>
|
||||
): Promise<Array<{ record: FileRecord; file: File; fileId: FileId; thumbnail?: string }>> {
|
||||
return Promise.all(
|
||||
files.map(async (file) => {
|
||||
const fileId = createFileId();
|
||||
filesRef.current.set(fileId, file);
|
||||
|
||||
// Generate thumbnail and page count for output file
|
||||
// Generate thumbnail and page count
|
||||
let thumbnail: string | undefined;
|
||||
let pageCount: number = 1;
|
||||
|
||||
try {
|
||||
if (DEBUG) console.log(`📄 consumeFiles: Generating thumbnail for output file ${file.name}`);
|
||||
if (DEBUG) console.log(`📄 Generating thumbnail for file ${file.name}`);
|
||||
const result = await generateThumbnailWithMetadata(file);
|
||||
thumbnail = result.thumbnail;
|
||||
pageCount = result.pageCount;
|
||||
} catch (error) {
|
||||
if (DEBUG) console.warn(`📄 consumeFiles: Failed to generate thumbnail for output file ${file.name}:`, error);
|
||||
if (DEBUG) console.warn(`📄 Failed to generate thumbnail for file ${file.name}:`, error);
|
||||
}
|
||||
|
||||
const record = toFileRecord(file, fileId);
|
||||
@ -357,24 +351,168 @@ export async function consumeFiles(
|
||||
record.processedFile = createProcessedFile(pageCount, thumbnail);
|
||||
}
|
||||
|
||||
return { file, id: fileId, thumbnail, record };
|
||||
return { record, file, fileId, thumbnail };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const outputFileRecords = processedOutputs.map(({ record }) => record);
|
||||
/**
|
||||
* Helper function to persist files to IndexedDB
|
||||
*/
|
||||
async function persistFilesToIndexedDB(
|
||||
fileRecords: Array<{ file: File; fileId: FileId; thumbnail?: string }>,
|
||||
indexedDB: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any> }
|
||||
): Promise<void> {
|
||||
await Promise.all(fileRecords.map(async ({ file, fileId, thumbnail }) => {
|
||||
try {
|
||||
await indexedDB.saveFile(file, fileId, thumbnail);
|
||||
} catch (error) {
|
||||
console.error('Failed to persist file to IndexedDB:', file.name, error);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume files helper - replace unpinned input files with output files
|
||||
*/
|
||||
export async function consumeFiles(
|
||||
inputFileIds: FileId[],
|
||||
outputFiles: File[],
|
||||
stateRef: React.MutableRefObject<FileContextState>,
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||
dispatch: React.Dispatch<FileContextAction>,
|
||||
indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any> } | null
|
||||
): Promise<FileId[]> {
|
||||
if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputFiles.length} output files`);
|
||||
|
||||
// Process output files with thumbnails and metadata
|
||||
const outputFileRecords = await processFilesIntoRecords(outputFiles, filesRef);
|
||||
|
||||
// Persist output files to IndexedDB if available
|
||||
if (indexedDB) {
|
||||
await persistFilesToIndexedDB(outputFileRecords, indexedDB);
|
||||
}
|
||||
|
||||
// Dispatch the consume action
|
||||
dispatch({
|
||||
type: 'CONSUME_FILES',
|
||||
payload: {
|
||||
inputFileIds,
|
||||
outputFileRecords
|
||||
outputFileRecords: outputFileRecords.map(({ record }) => record)
|
||||
}
|
||||
});
|
||||
|
||||
if (DEBUG) console.log(`📄 consumeFiles: Successfully consumed files - removed ${inputFileIds.length} inputs, added ${outputFileRecords.length} outputs`);
|
||||
|
||||
return processedOutputs.map(({ file, id, thumbnail }) => ({ file, id, thumbnail }));
|
||||
// Return the output file IDs for undo tracking
|
||||
return outputFileRecords.map(({ fileId }) => fileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to restore files to filesRef and manage IndexedDB cleanup
|
||||
*/
|
||||
async function restoreFilesAndCleanup(
|
||||
filesToRestore: Array<{ file: File; record: FileRecord }>,
|
||||
fileIdsToRemove: FileId[],
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||
indexedDB?: { deleteFile: (fileId: FileId) => Promise<void> } | null
|
||||
): Promise<void> {
|
||||
// Remove files from filesRef
|
||||
fileIdsToRemove.forEach(id => {
|
||||
if (filesRef.current.has(id)) {
|
||||
if (DEBUG) console.log(`📄 Removing file ${id} from filesRef`);
|
||||
filesRef.current.delete(id);
|
||||
} else {
|
||||
if (DEBUG) console.warn(`📄 File ${id} not found in filesRef`);
|
||||
}
|
||||
});
|
||||
|
||||
// Restore files to filesRef
|
||||
filesToRestore.forEach(({ file, record }) => {
|
||||
if (file && record) {
|
||||
// Validate the file before restoring
|
||||
if (file.size === 0) {
|
||||
if (DEBUG) console.warn(`📄 Skipping empty file ${file.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore the file to filesRef
|
||||
if (DEBUG) console.log(`📄 Restoring file ${file.name} with id ${record.id} to filesRef`);
|
||||
filesRef.current.set(record.id, file);
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up IndexedDB
|
||||
if (indexedDB) {
|
||||
const indexedDBPromises = fileIdsToRemove.map(fileId =>
|
||||
indexedDB.deleteFile(fileId).catch(error => {
|
||||
console.error('Failed to delete file from IndexedDB:', fileId, error);
|
||||
throw error; // Re-throw to trigger rollback
|
||||
})
|
||||
);
|
||||
|
||||
// Execute all IndexedDB operations
|
||||
await Promise.all(indexedDBPromises);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undoes a previous consumeFiles operation by restoring input files and removing output files (unless pinned)
|
||||
*/
|
||||
export async function undoConsumeFiles(
|
||||
inputFiles: File[],
|
||||
inputFileRecords: FileRecord[],
|
||||
outputFileIds: FileId[],
|
||||
stateRef: React.MutableRefObject<FileContextState>,
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||
dispatch: React.Dispatch<FileContextAction>,
|
||||
indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any>; deleteFile: (fileId: FileId) => Promise<void> } | null
|
||||
): Promise<void> {
|
||||
if (DEBUG) console.log(`📄 undoConsumeFiles: Restoring ${inputFileRecords.length} input files, removing ${outputFileIds.length} output files`);
|
||||
|
||||
// Validate inputs
|
||||
if (inputFiles.length !== inputFileRecords.length) {
|
||||
throw new Error(`Mismatch between input files (${inputFiles.length}) and records (${inputFileRecords.length})`);
|
||||
}
|
||||
|
||||
// Create a backup of current filesRef state for rollback
|
||||
const backupFilesRef = new Map(filesRef.current);
|
||||
|
||||
try {
|
||||
// Prepare files to restore
|
||||
const filesToRestore = inputFiles.map((file, index) => ({
|
||||
file,
|
||||
record: inputFileRecords[index]
|
||||
}));
|
||||
|
||||
// Restore input files and clean up output files
|
||||
await restoreFilesAndCleanup(
|
||||
filesToRestore,
|
||||
outputFileIds,
|
||||
filesRef,
|
||||
indexedDB
|
||||
);
|
||||
|
||||
// Dispatch the undo action (only if everything else succeeded)
|
||||
dispatch({
|
||||
type: 'UNDO_CONSUME_FILES',
|
||||
payload: {
|
||||
inputFileRecords,
|
||||
outputFileIds
|
||||
}
|
||||
});
|
||||
|
||||
if (DEBUG) console.log(`📄 undoConsumeFiles: Successfully undone consume operation - restored ${inputFileRecords.length} inputs, removed ${outputFileIds.length} outputs`);
|
||||
|
||||
} catch (error) {
|
||||
// Rollback filesRef to previous state
|
||||
if (DEBUG) console.error('📄 undoConsumeFiles: Error during undo, rolling back filesRef', error);
|
||||
filesRef.current.clear();
|
||||
backupFilesRef.forEach((file, id) => {
|
||||
filesRef.current.set(id, file);
|
||||
});
|
||||
throw error; // Re-throw to let caller handle
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -164,6 +164,21 @@ export function useFileContext() {
|
||||
// File management
|
||||
addFiles: actions.addFiles,
|
||||
consumeFiles: actions.consumeFiles,
|
||||
undoConsumeFiles: actions.undoConsumeFiles,
|
||||
recordOperation: (fileId: FileId, operation: any) => {}, // Operation tracking not implemented
|
||||
markOperationApplied: (fileId: FileId, operationId: string) => {}, // Operation tracking not implemented
|
||||
markOperationFailed: (fileId: FileId, operationId: string, error: string) => {}, // Operation tracking not implemented
|
||||
|
||||
// File ID lookup
|
||||
findFileId: (file: File) => {
|
||||
return state.files.ids.find(id => {
|
||||
const record = state.files.byId[id];
|
||||
return record &&
|
||||
record.name === file.name &&
|
||||
record.size === file.size &&
|
||||
record.lastModified === file.lastModified;
|
||||
});
|
||||
},
|
||||
|
||||
// Pinned files
|
||||
pinnedFiles: state.pinnedFiles,
|
||||
@ -172,7 +187,11 @@ export function useFileContext() {
|
||||
isFilePinned: selectors.isFilePinned,
|
||||
|
||||
// Active files
|
||||
activeFiles: selectors.getFiles()
|
||||
activeFiles: selectors.getFiles(),
|
||||
|
||||
// Direct access to actions and selectors (for advanced use cases)
|
||||
actions,
|
||||
selectors
|
||||
}), [state, selectors, actions]);
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,7 @@ describe('useAddPasswordOperation', () => {
|
||||
resetResults: vi.fn(),
|
||||
clearError: vi.fn(),
|
||||
cancelOperation: vi.fn(),
|
||||
undoOperation: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -41,6 +41,7 @@ describe('useChangePermissionsOperation', () => {
|
||||
resetResults: vi.fn(),
|
||||
clearError: vi.fn(),
|
||||
cancelOperation: vi.fn(),
|
||||
undoOperation: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -41,6 +41,7 @@ describe('useRemovePasswordOperation', () => {
|
||||
resetResults: vi.fn(),
|
||||
clearError: vi.fn(),
|
||||
cancelOperation: vi.fn(),
|
||||
undoOperation: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
126
frontend/src/hooks/tools/shared/useBaseTool.ts
Normal file
126
frontend/src/hooks/tools/shared/useBaseTool.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useFileSelection } from '../../../contexts/FileContext';
|
||||
import { useEndpointEnabled } from '../../useEndpointConfig';
|
||||
import { BaseToolProps } from '../../../types/tool';
|
||||
import { ToolOperationHook } from './useToolOperation';
|
||||
import { BaseParametersHook } from './useBaseParameters';
|
||||
import { FileWithId } from '../../../types/fileContext';
|
||||
|
||||
interface BaseToolReturn<TParams> {
|
||||
// File management
|
||||
selectedFiles: FileWithId[];
|
||||
|
||||
// Tool-specific hooks
|
||||
params: BaseParametersHook<TParams>;
|
||||
operation: ToolOperationHook<TParams>;
|
||||
|
||||
// Endpoint validation
|
||||
endpointEnabled: boolean | null;
|
||||
endpointLoading: boolean;
|
||||
|
||||
// Standard handlers
|
||||
handleExecute: () => Promise<void>;
|
||||
handleThumbnailClick: (file: File) => void;
|
||||
handleSettingsReset: () => void;
|
||||
handleUndo: () => Promise<void>;
|
||||
|
||||
// Standard computed state
|
||||
hasFiles: boolean;
|
||||
hasResults: boolean;
|
||||
settingsCollapsed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base tool hook for tool components. Manages standard behaviour for tools.
|
||||
*/
|
||||
export function useBaseTool<TParams>(
|
||||
toolName: string,
|
||||
useParams: () => BaseParametersHook<TParams>,
|
||||
useOperation: () => ToolOperationHook<TParams>,
|
||||
props: BaseToolProps,
|
||||
): BaseToolReturn<TParams> {
|
||||
const { onPreviewFile, onComplete, onError } = props;
|
||||
|
||||
// File selection
|
||||
const { selectedFiles } = useFileSelection();
|
||||
|
||||
// Tool-specific hooks
|
||||
const params = useParams();
|
||||
const operation = useOperation();
|
||||
|
||||
// Endpoint validation using parameters hook
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(params.getEndpointName());
|
||||
|
||||
// Reset results when parameters change
|
||||
useEffect(() => {
|
||||
operation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [params.parameters]);
|
||||
|
||||
// Reset results when selected files change
|
||||
useEffect(() => {
|
||||
if (selectedFiles.length > 0) {
|
||||
operation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}
|
||||
}, [selectedFiles.length]);
|
||||
|
||||
// Standard handlers
|
||||
const handleExecute = useCallback(async () => {
|
||||
try {
|
||||
await operation.executeOperation(params.parameters, selectedFiles);
|
||||
if (operation.files && onComplete) {
|
||||
onComplete(operation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
const message = error instanceof Error ? error.message : `${toolName} operation failed`;
|
||||
onError(message);
|
||||
}
|
||||
}
|
||||
}, [operation, params.parameters, selectedFiles, onComplete, onError, toolName]);
|
||||
|
||||
const handleThumbnailClick = useCallback((file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem('previousMode', toolName);
|
||||
}, [onPreviewFile, toolName]);
|
||||
|
||||
const handleSettingsReset = useCallback(() => {
|
||||
operation.resetResults();
|
||||
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;
|
||||
const settingsCollapsed = !hasFiles || hasResults;
|
||||
|
||||
return {
|
||||
// File management
|
||||
selectedFiles,
|
||||
|
||||
// Tool-specific hooks
|
||||
params,
|
||||
operation,
|
||||
|
||||
// Endpoint validation
|
||||
endpointEnabled,
|
||||
endpointLoading,
|
||||
|
||||
// Handlers
|
||||
handleExecute,
|
||||
handleThumbnailClick,
|
||||
handleSettingsReset,
|
||||
handleUndo,
|
||||
|
||||
// State
|
||||
hasFiles,
|
||||
hasResults,
|
||||
settingsCollapsed
|
||||
};
|
||||
}
|
@ -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';
|
||||
@ -6,7 +6,7 @@ import { useToolState, type ProcessingProgress } from './useToolState';
|
||||
import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls';
|
||||
import { useToolResources } from './useToolResources';
|
||||
import { extractErrorMessage } from '../../../utils/toolErrorHandler';
|
||||
import { FileWithId, extractFiles } from '../../../types/fileContext';
|
||||
import { FileWithId, extractFiles, FileId, FileRecord } from '../../../types/fileContext';
|
||||
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
@ -86,6 +86,7 @@ export interface ToolOperationHook<TParams = void> {
|
||||
resetResults: () => void;
|
||||
clearError: () => void;
|
||||
cancelOperation: () => void;
|
||||
undoOperation: () => Promise<void>;
|
||||
}
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
@ -107,13 +108,20 @@ export const useToolOperation = <TParams = void>(
|
||||
config: ToolOperationConfig<TParams>
|
||||
): ToolOperationHook<TParams> => {
|
||||
const { t } = useTranslation();
|
||||
const { addFiles, consumeFiles } = 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: FileWithId[]
|
||||
@ -158,7 +166,7 @@ export const useToolOperation = <TParams = void>(
|
||||
// Multi-file responses are typically ZIP files that need extraction, but some may return single PDFs
|
||||
if (config.responseHandler) {
|
||||
// Use custom responseHandler for multi-file (handles ZIP extraction)
|
||||
processedFiles = await config.responseHandler(response.data, validRegularFiles);
|
||||
processedFiles = await config.responseHandler(response.data, validFiles);
|
||||
} else if (response.data.type === 'application/pdf' ||
|
||||
(response.headers && response.headers['content-type'] === 'application/pdf')) {
|
||||
// Single PDF response (e.g. split with merge option) - use original filename
|
||||
@ -207,8 +215,34 @@ export const useToolOperation = <TParams = void>(
|
||||
actions.setDownloadInfo(downloadInfo.url, downloadInfo.filename);
|
||||
|
||||
// Replace input files with processed files (consumeFiles handles pinning)
|
||||
const inputFileIds = validFiles.map(file => file.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
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
@ -231,8 +265,65 @@ export const useToolOperation = <TParams = void>(
|
||||
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,
|
||||
@ -249,6 +340,7 @@ export const useToolOperation = <TParams = void>(
|
||||
executeOperation,
|
||||
resetResults,
|
||||
clearError: actions.clearError,
|
||||
cancelOperation
|
||||
cancelOperation,
|
||||
undoOperation
|
||||
};
|
||||
};
|
||||
|
@ -9,12 +9,12 @@ export const useFileHandler = () => {
|
||||
|
||||
const addToActiveFiles = useCallback(async (file: File) => {
|
||||
// Let FileContext handle deduplication with quickKey logic
|
||||
await actions.addFiles([file]);
|
||||
await actions.addFiles([file], { selectFiles: true });
|
||||
}, [actions.addFiles]);
|
||||
|
||||
const addMultipleFiles = useCallback(async (files: File[]) => {
|
||||
// Let FileContext handle deduplication with quickKey logic
|
||||
await actions.addFiles(files);
|
||||
await actions.addFiles(files, { selectFiles: true });
|
||||
}, [actions.addFiles]);
|
||||
|
||||
// Add stored files preserving their original IDs to prevent session duplicates
|
||||
@ -29,7 +29,7 @@ export const useFileHandler = () => {
|
||||
file,
|
||||
originalId: originalId as FileId,
|
||||
metadata
|
||||
})));
|
||||
})), { selectFiles: true });
|
||||
}
|
||||
|
||||
console.log(`📁 Added ${newFiles.length} stored files (${filesWithMetadata.length - newFiles.length} skipped as duplicates)`);
|
||||
|
@ -60,6 +60,11 @@ const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
const handleUndo = async () => {
|
||||
await addPasswordOperation.undoOperation();
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = addPasswordOperation.files.length > 0 || addPasswordOperation.downloadUrl !== null;
|
||||
const passwordsCollapsed = !hasFiles || hasResults;
|
||||
@ -110,6 +115,7 @@ const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
operation: addPasswordOperation,
|
||||
title: t("addPassword.results.title", "Encrypted PDFs"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
onUndo: handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -79,6 +79,11 @@ const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) =>
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
const handleUndo = async () => {
|
||||
await watermarkOperation.undoOperation();
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = watermarkOperation.files.length > 0 || watermarkOperation.downloadUrl !== null;
|
||||
|
||||
@ -203,6 +208,7 @@ const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) =>
|
||||
operation: watermarkOperation,
|
||||
title: t("watermark.results.title", "Watermark Results"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
onUndo: handleUndo,
|
||||
},
|
||||
forceStepNumbers: true,
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import React, { useState, useMemo, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFileContext } from "../contexts/FileContext";
|
||||
import { useFileSelection } from "../contexts/FileContext";
|
||||
import { useNavigation } from "../contexts/NavigationContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
import { useToolWorkflow } from "../contexts/ToolWorkflowContext";
|
||||
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
@ -21,7 +21,7 @@ import { AUTOMATION_STEPS } from "../constants/automation";
|
||||
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { selectedFiles } = useFileSelection();
|
||||
const { setMode } = useNavigation();
|
||||
const { actions } = useNavigationActions();
|
||||
const { registerToolReset } = useToolWorkflow();
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<AutomationStep>(AUTOMATION_STEPS.SELECTION);
|
||||
@ -43,6 +43,11 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
setStepData({ step: AUTOMATION_STEPS.SELECTION });
|
||||
};
|
||||
|
||||
const handleUndo = async () => {
|
||||
await automateOperation.undoOperation();
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
// Register reset function with the tool workflow context - only once on mount
|
||||
React.useEffect(() => {
|
||||
const stableResetFunction = () => {
|
||||
@ -223,8 +228,9 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
title: t('automate.reviewTitle', 'Automation Results'),
|
||||
onFileClick: (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
setMode('viewer');
|
||||
}
|
||||
actions.setWorkbench('viewer');
|
||||
},
|
||||
onUndo: handleUndo
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1,96 +1,56 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileSelection } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
|
||||
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
|
||||
|
||||
import { useChangePermissionsParameters } from "../hooks/tools/changePermissions/useChangePermissionsParameters";
|
||||
import { useChangePermissionsOperation } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
|
||||
import { useChangePermissionsTips } from "../components/tooltips/useChangePermissionsTips";
|
||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||
|
||||
const ChangePermissions = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const ChangePermissions = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { actions } = useNavigationActions();
|
||||
const { selectedFiles } = useFileSelection();
|
||||
|
||||
const changePermissionsParams = useChangePermissionsParameters();
|
||||
const changePermissionsOperation = useChangePermissionsOperation();
|
||||
const changePermissionsTips = useChangePermissionsTips();
|
||||
|
||||
// Endpoint validation
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(changePermissionsParams.getEndpointName());
|
||||
|
||||
useEffect(() => {
|
||||
changePermissionsOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [changePermissionsParams.parameters]);
|
||||
|
||||
const handleChangePermissions = async () => {
|
||||
try {
|
||||
await changePermissionsOperation.executeOperation(changePermissionsParams.parameters, selectedFiles);
|
||||
if (changePermissionsOperation.files && onComplete) {
|
||||
onComplete(changePermissionsOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(
|
||||
error instanceof Error ? error.message : t("changePermissions.error.failed", "Change permissions operation failed")
|
||||
const base = useBaseTool(
|
||||
'changePermissions',
|
||||
useChangePermissionsParameters,
|
||||
useChangePermissionsOperation,
|
||||
props
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem("previousMode", "changePermissions");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
changePermissionsOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = changePermissionsOperation.files.length > 0 || changePermissionsOperation.downloadUrl !== null;
|
||||
const settingsCollapsed = !hasFiles || hasResults;
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: hasResults,
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasResults,
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: t("changePermissions.title", "Document Permissions"),
|
||||
isCollapsed: settingsCollapsed,
|
||||
onCollapsedClick: settingsCollapsed ? handleSettingsReset : undefined,
|
||||
isCollapsed: base.settingsCollapsed,
|
||||
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||
tooltip: changePermissionsTips,
|
||||
content: (
|
||||
<ChangePermissionsSettings
|
||||
parameters={changePermissionsParams.parameters}
|
||||
onParameterChange={changePermissionsParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
parameters={base.params.parameters}
|
||||
onParameterChange={base.params.updateParameter}
|
||||
disabled={base.endpointLoading}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t("changePermissions.submit", "Change Permissions"),
|
||||
isVisible: !hasResults,
|
||||
isVisible: !base.hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: handleChangePermissions,
|
||||
disabled: !changePermissionsParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
onClick: base.handleExecute,
|
||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: changePermissionsOperation,
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: t("changePermissions.results.title", "Modified PDFs"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,95 +1,56 @@
|
||||
import React, { use, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileSelection } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
|
||||
import CompressSettings from "../components/tools/compress/CompressSettings";
|
||||
|
||||
import { useCompressParameters } from "../hooks/tools/compress/useCompressParameters";
|
||||
import { useCompressOperation } from "../hooks/tools/compress/useCompressOperation";
|
||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||
import { useCompressTips } from "../components/tooltips/useCompressTips";
|
||||
|
||||
const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const Compress = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { actions } = useNavigationActions();
|
||||
const { selectedFiles } = useFileSelection();
|
||||
|
||||
const compressParams = useCompressParameters();
|
||||
const compressOperation = useCompressOperation();
|
||||
const compressTips = useCompressTips();
|
||||
|
||||
// Endpoint validation
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("compress-pdf");
|
||||
|
||||
useEffect(() => {
|
||||
compressOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [compressParams.parameters]);
|
||||
|
||||
const handleCompress = async () => {
|
||||
try {
|
||||
await compressOperation.executeOperation(compressParams.parameters, selectedFiles);
|
||||
if (compressOperation.files && onComplete) {
|
||||
onComplete(compressOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error.message : "Compress operation failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem("previousMode", "compress");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
compressOperation.resetResults();
|
||||
onPreviewFile?.(null); };
|
||||
|
||||
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = compressOperation.files.length > 0 || compressOperation.downloadUrl !== null;
|
||||
const settingsCollapsed = !hasFiles || hasResults;
|
||||
const base = useBaseTool(
|
||||
'compress',
|
||||
useCompressParameters,
|
||||
useCompressOperation,
|
||||
props
|
||||
);
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: hasResults,
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasResults,
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: "Settings",
|
||||
isCollapsed: settingsCollapsed,
|
||||
onCollapsedClick: settingsCollapsed ? handleSettingsReset : undefined,
|
||||
isCollapsed: base.settingsCollapsed,
|
||||
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||
tooltip: compressTips,
|
||||
content: (
|
||||
<CompressSettings
|
||||
parameters={compressParams.parameters}
|
||||
onParameterChange={compressParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
parameters={base.params.parameters}
|
||||
onParameterChange={base.params.updateParameter}
|
||||
disabled={base.endpointLoading}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t("compress.submit", "Compress"),
|
||||
isVisible: !hasResults,
|
||||
isVisible: !base.hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: handleCompress,
|
||||
disabled: !compressParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
onClick: base.handleExecute,
|
||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: compressOperation,
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: t("compress.title", "Compression Results"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -93,6 +93,11 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
const handleUndo = async () => {
|
||||
await convertOperation.undoOperation();
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
@ -128,6 +133,7 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
operation: convertOperation,
|
||||
title: t("convert.conversionResults", "Conversion Results"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
onUndo: handleUndo,
|
||||
testId: "conversion-results",
|
||||
},
|
||||
});
|
||||
|
@ -74,6 +74,11 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
const handleUndo = async () => {
|
||||
await ocrOperation.undoOperation();
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
const settingsCollapsed = expandedStep !== "settings";
|
||||
|
||||
return createToolFlow({
|
||||
@ -132,6 +137,7 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
operation: ocrOperation,
|
||||
title: t("ocr.results.title", "OCR Results"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
onUndo: handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,78 +1,40 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileContext } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
import { useFileSelection } from "../contexts/file/fileHooks";
|
||||
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
|
||||
import { useRemoveCertificateSignParameters } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters";
|
||||
import { useRemoveCertificateSignOperation } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
|
||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||
|
||||
const RemoveCertificateSign = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const RemoveCertificateSign = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { actions } = useNavigationActions();
|
||||
const { selectedFiles } = useFileSelection();
|
||||
|
||||
const removeCertificateSignParams = useRemoveCertificateSignParameters();
|
||||
const removeCertificateSignOperation = useRemoveCertificateSignOperation();
|
||||
|
||||
// Endpoint validation
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(removeCertificateSignParams.getEndpointName());
|
||||
|
||||
useEffect(() => {
|
||||
removeCertificateSignOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [removeCertificateSignParams.parameters]);
|
||||
|
||||
const handleRemoveSignature = async () => {
|
||||
try {
|
||||
await removeCertificateSignOperation.executeOperation(removeCertificateSignParams.parameters, selectedFiles);
|
||||
if (removeCertificateSignOperation.files && onComplete) {
|
||||
onComplete(removeCertificateSignOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error.message : t("removeCertSign.error.failed", "Remove certificate signature operation failed"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem("previousMode", "removeCertificateSign");
|
||||
actions.setMode("viewer");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
removeCertificateSignOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = removeCertificateSignOperation.files.length > 0 || removeCertificateSignOperation.downloadUrl !== null;
|
||||
const base = useBaseTool(
|
||||
'removeCertificateSign',
|
||||
useRemoveCertificateSignParameters,
|
||||
useRemoveCertificateSignOperation,
|
||||
props
|
||||
);
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: hasFiles || hasResults,
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasResults,
|
||||
placeholder: t("removeCertSign.files.placeholder", "Select a PDF file in the main view to get started"),
|
||||
},
|
||||
steps: [],
|
||||
executeButton: {
|
||||
text: t("removeCertSign.submit", "Remove Signature"),
|
||||
isVisible: !hasResults,
|
||||
isVisible: !base.hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: handleRemoveSignature,
|
||||
disabled: !removeCertificateSignParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
onClick: base.handleExecute,
|
||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: removeCertificateSignOperation,
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: t("removeCertSign.results.title", "Certificate Removal Results"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,95 +1,56 @@
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileSelection } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
|
||||
import RemovePasswordSettings from "../components/tools/removePassword/RemovePasswordSettings";
|
||||
|
||||
import { useRemovePasswordParameters } from "../hooks/tools/removePassword/useRemovePasswordParameters";
|
||||
import { useRemovePasswordOperation } from "../hooks/tools/removePassword/useRemovePasswordOperation";
|
||||
import { useRemovePasswordTips } from "../components/tooltips/useRemovePasswordTips";
|
||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||
|
||||
const RemovePassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const RemovePassword = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { actions } = useNavigationActions();
|
||||
const { selectedFiles } = useFileSelection();
|
||||
|
||||
const removePasswordParams = useRemovePasswordParameters();
|
||||
const removePasswordOperation = useRemovePasswordOperation();
|
||||
const removePasswordTips = useRemovePasswordTips();
|
||||
|
||||
// Endpoint validation
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(removePasswordParams.getEndpointName());
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
removePasswordOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [removePasswordParams.parameters]);
|
||||
|
||||
const handleRemovePassword = async () => {
|
||||
try {
|
||||
await removePasswordOperation.executeOperation(removePasswordParams.parameters, selectedFiles);
|
||||
if (removePasswordOperation.files && onComplete) {
|
||||
onComplete(removePasswordOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error.message : t("removePassword.error.failed", "Remove password operation failed"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem("previousMode", "removePassword");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
removePasswordOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = removePasswordOperation.files.length > 0 || removePasswordOperation.downloadUrl !== null;
|
||||
const passwordCollapsed = !hasFiles || hasResults;
|
||||
const base = useBaseTool(
|
||||
'removePassword',
|
||||
useRemovePasswordParameters,
|
||||
useRemovePasswordOperation,
|
||||
props
|
||||
);
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: hasResults,
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasResults,
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: t("removePassword.password.stepTitle", "Remove Password"),
|
||||
isCollapsed: passwordCollapsed,
|
||||
onCollapsedClick: hasResults ? handleSettingsReset : undefined,
|
||||
isCollapsed: base.settingsCollapsed,
|
||||
onCollapsedClick: base.hasResults ? base.handleSettingsReset : undefined,
|
||||
tooltip: removePasswordTips,
|
||||
content: (
|
||||
<RemovePasswordSettings
|
||||
parameters={removePasswordParams.parameters}
|
||||
onParameterChange={removePasswordParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
parameters={base.params.parameters}
|
||||
onParameterChange={base.params.updateParameter}
|
||||
disabled={base.endpointLoading}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t("removePassword.submit", "Remove Password"),
|
||||
isVisible: !hasResults,
|
||||
isVisible: !base.hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: handleRemovePassword,
|
||||
disabled: !removePasswordParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
onClick: base.handleExecute,
|
||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: removePasswordOperation,
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: t("removePassword.results.title", "Decrypted PDFs"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,78 +1,40 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileContext } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
import { useFileSelection } from "../contexts/file/fileHooks";
|
||||
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
|
||||
import { useRepairParameters } from "../hooks/tools/repair/useRepairParameters";
|
||||
import { useRepairOperation } from "../hooks/tools/repair/useRepairOperation";
|
||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||
|
||||
const Repair = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const Repair = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { actions } = useNavigationActions();
|
||||
const { selectedFiles } = useFileSelection();
|
||||
|
||||
const repairParams = useRepairParameters();
|
||||
const repairOperation = useRepairOperation();
|
||||
|
||||
// Endpoint validation
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(repairParams.getEndpointName());
|
||||
|
||||
useEffect(() => {
|
||||
repairOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [repairParams.parameters]);
|
||||
|
||||
const handleRepair = async () => {
|
||||
try {
|
||||
await repairOperation.executeOperation(repairParams.parameters, selectedFiles);
|
||||
if (repairOperation.files && onComplete) {
|
||||
onComplete(repairOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error.message : t("repair.error.failed", "Repair operation failed"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem("previousMode", "repair");
|
||||
actions.setMode("viewer");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
repairOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = repairOperation.files.length > 0 || repairOperation.downloadUrl !== null;
|
||||
const base = useBaseTool(
|
||||
'repair',
|
||||
useRepairParameters,
|
||||
useRepairOperation,
|
||||
props
|
||||
);
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: hasResults,
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasResults,
|
||||
placeholder: t("repair.files.placeholder", "Select a PDF file in the main view to get started"),
|
||||
},
|
||||
steps: [],
|
||||
executeButton: {
|
||||
text: t("repair.submit", "Repair PDF"),
|
||||
isVisible: !hasResults,
|
||||
isVisible: !base.hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: handleRepair,
|
||||
disabled: !repairParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
onClick: base.handleExecute,
|
||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: repairOperation,
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: t("repair.results.title", "Repair Results"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,90 +1,54 @@
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileSelection } from "../contexts/FileContext";
|
||||
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings";
|
||||
|
||||
import { useSanitizeParameters } from "../hooks/tools/sanitize/useSanitizeParameters";
|
||||
import { useSanitizeOperation } from "../hooks/tools/sanitize/useSanitizeOperation";
|
||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||
|
||||
const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const Sanitize = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { selectedFiles } = useFileSelection();
|
||||
|
||||
const sanitizeParams = useSanitizeParameters();
|
||||
const sanitizeOperation = useSanitizeOperation();
|
||||
|
||||
// Endpoint validation
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(sanitizeParams.getEndpointName());
|
||||
|
||||
useEffect(() => {
|
||||
sanitizeOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [sanitizeParams.parameters]);
|
||||
|
||||
const handleSanitize = async () => {
|
||||
try {
|
||||
await sanitizeOperation.executeOperation(sanitizeParams.parameters, selectedFiles);
|
||||
if (sanitizeOperation.files && onComplete) {
|
||||
onComplete(sanitizeOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error.message : t("sanitize.error.generic", "Sanitization failed"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
sanitizeOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem("previousMode", "sanitize");
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = sanitizeOperation.files.length > 0;
|
||||
const settingsCollapsed = !hasFiles || hasResults;
|
||||
const base = useBaseTool(
|
||||
'sanitize',
|
||||
useSanitizeParameters,
|
||||
useSanitizeOperation,
|
||||
props
|
||||
);
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: hasResults,
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasResults,
|
||||
placeholder: t("sanitize.files.placeholder", "Select a PDF file in the main view to get started"),
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: t("sanitize.steps.settings", "Settings"),
|
||||
isCollapsed: settingsCollapsed,
|
||||
onCollapsedClick: settingsCollapsed ? handleSettingsReset : undefined,
|
||||
isCollapsed: base.settingsCollapsed,
|
||||
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||
content: (
|
||||
<SanitizeSettings
|
||||
parameters={sanitizeParams.parameters}
|
||||
onParameterChange={sanitizeParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
parameters={base.params.parameters}
|
||||
onParameterChange={base.params.updateParameter}
|
||||
disabled={base.endpointLoading}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t("sanitize.submit", "Sanitize PDF"),
|
||||
isVisible: !hasResults,
|
||||
isVisible: !base.hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: handleSanitize,
|
||||
disabled: !sanitizeParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
onClick: base.handleExecute,
|
||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: sanitizeOperation,
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: t("sanitize.sanitizationResults", "Sanitization Results"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,78 +1,40 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileContext } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
import { useFileSelection } from "../contexts/file/fileHooks";
|
||||
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
|
||||
import { useSingleLargePageParameters } from "../hooks/tools/singleLargePage/useSingleLargePageParameters";
|
||||
import { useSingleLargePageOperation } from "../hooks/tools/singleLargePage/useSingleLargePageOperation";
|
||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||
|
||||
const SingleLargePage = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const SingleLargePage = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { actions } = useNavigationActions();
|
||||
const { selectedFiles } = useFileSelection();
|
||||
|
||||
const singleLargePageParams = useSingleLargePageParameters();
|
||||
const singleLargePageOperation = useSingleLargePageOperation();
|
||||
|
||||
// Endpoint validation
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(singleLargePageParams.getEndpointName());
|
||||
|
||||
useEffect(() => {
|
||||
singleLargePageOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [singleLargePageParams.parameters]);
|
||||
|
||||
const handleConvert = async () => {
|
||||
try {
|
||||
await singleLargePageOperation.executeOperation(singleLargePageParams.parameters, selectedFiles);
|
||||
if (singleLargePageOperation.files && onComplete) {
|
||||
onComplete(singleLargePageOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error.message : t("pdfToSinglePage.error.failed", "Single large page operation failed"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem("previousMode", "single-large-page");
|
||||
actions.setMode("viewer");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
singleLargePageOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = singleLargePageOperation.files.length > 0 || singleLargePageOperation.downloadUrl !== null;
|
||||
const base = useBaseTool(
|
||||
'singleLargePage',
|
||||
useSingleLargePageParameters,
|
||||
useSingleLargePageOperation,
|
||||
props
|
||||
);
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: hasFiles || hasResults,
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasResults,
|
||||
placeholder: t("pdfToSinglePage.files.placeholder", "Select a PDF file in the main view to get started"),
|
||||
},
|
||||
steps: [],
|
||||
executeButton: {
|
||||
text: t("pdfToSinglePage.submit", "Convert To Single Page"),
|
||||
isVisible: !hasResults,
|
||||
isVisible: !base.hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: handleConvert,
|
||||
disabled: !singleLargePageParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
onClick: base.handleExecute,
|
||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: singleLargePageOperation,
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: t("pdfToSinglePage.results.title", "Single Page Results"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,84 +1,37 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileSelection } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
import SplitSettings from "../components/tools/split/SplitSettings";
|
||||
|
||||
import { useSplitParameters } from "../hooks/tools/split/useSplitParameters";
|
||||
import { useSplitOperation } from "../hooks/tools/split/useSplitOperation";
|
||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||
|
||||
const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const Split = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { actions } = useNavigationActions();
|
||||
const { selectedFiles } = useFileSelection();
|
||||
|
||||
const splitParams = useSplitParameters();
|
||||
const splitOperation = useSplitOperation();
|
||||
|
||||
// Endpoint validation
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(splitParams.getEndpointName());
|
||||
|
||||
useEffect(() => {
|
||||
// Only reset results when parameters change, not when files change
|
||||
splitOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [splitParams.parameters]);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset results when selected files change (user selected different files)
|
||||
if (selectedFiles.length > 0) {
|
||||
splitOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}
|
||||
}, [selectedFiles]);
|
||||
|
||||
const handleSplit = async () => {
|
||||
try {
|
||||
await splitOperation.executeOperation(splitParams.parameters, selectedFiles);
|
||||
if (splitOperation.files && onComplete) {
|
||||
onComplete(splitOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error.message : "Split operation failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem("previousMode", "split");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
splitOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = splitOperation.files.length > 0 || splitOperation.downloadUrl !== null;
|
||||
const settingsCollapsed = !hasFiles || hasResults;
|
||||
const base = useBaseTool(
|
||||
'split',
|
||||
useSplitParameters,
|
||||
useSplitOperation,
|
||||
props
|
||||
);
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: hasResults,
|
||||
placeholder: "Select a PDF file in the main view to get started",
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasResults,
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: "Settings",
|
||||
isCollapsed: settingsCollapsed,
|
||||
onCollapsedClick: hasResults ? handleSettingsReset : undefined,
|
||||
isCollapsed: base.settingsCollapsed,
|
||||
onCollapsedClick: base.hasResults ? base.handleSettingsReset : undefined,
|
||||
content: (
|
||||
<SplitSettings
|
||||
parameters={splitParams.parameters}
|
||||
onParameterChange={splitParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
parameters={base.params.parameters}
|
||||
onParameterChange={base.params.updateParameter}
|
||||
disabled={base.endpointLoading}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@ -86,15 +39,16 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
executeButton: {
|
||||
text: t("split.submit", "Split PDF"),
|
||||
loadingText: t("loading"),
|
||||
onClick: handleSplit,
|
||||
isVisible: !hasResults,
|
||||
disabled: !splitParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
onClick: base.handleExecute,
|
||||
isVisible: !base.hasResults,
|
||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: splitOperation,
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: "Split Results",
|
||||
onFileClick: handleThumbnailClick,
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,78 +1,40 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileContext } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
import { useFileSelection } from "../contexts/file/fileHooks";
|
||||
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
|
||||
import { useUnlockPdfFormsParameters } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsParameters";
|
||||
import { useUnlockPdfFormsOperation } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation";
|
||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||
|
||||
const UnlockPdfForms = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const UnlockPdfForms = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { actions } = useNavigationActions();
|
||||
const { selectedFiles } = useFileSelection();
|
||||
|
||||
const unlockPdfFormsParams = useUnlockPdfFormsParameters();
|
||||
const unlockPdfFormsOperation = useUnlockPdfFormsOperation();
|
||||
|
||||
// Endpoint validation
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(unlockPdfFormsParams.getEndpointName());
|
||||
|
||||
useEffect(() => {
|
||||
unlockPdfFormsOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [unlockPdfFormsParams.parameters]);
|
||||
|
||||
const handleUnlock = async () => {
|
||||
try {
|
||||
await unlockPdfFormsOperation.executeOperation(unlockPdfFormsParams.parameters, selectedFiles);
|
||||
if (unlockPdfFormsOperation.files && onComplete) {
|
||||
onComplete(unlockPdfFormsOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error.message : t("unlockPDFForms.error.failed", "Unlock PDF forms operation failed"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem("previousMode", "unlockPdfForms");
|
||||
actions.setMode("viewer");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
unlockPdfFormsOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = unlockPdfFormsOperation.files.length > 0 || unlockPdfFormsOperation.downloadUrl !== null;
|
||||
const base = useBaseTool(
|
||||
'unlockPdfForms',
|
||||
useUnlockPdfFormsParameters,
|
||||
useUnlockPdfFormsOperation,
|
||||
props
|
||||
);
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: hasFiles || hasResults,
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasFiles || base.hasResults,
|
||||
placeholder: t("unlockPDFForms.files.placeholder", "Select a PDF file in the main view to get started"),
|
||||
},
|
||||
steps: [],
|
||||
executeButton: {
|
||||
text: t("unlockPDFForms.submit", "Unlock Forms"),
|
||||
isVisible: !hasResults,
|
||||
isVisible: !base.hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: handleUnlock,
|
||||
disabled: !unlockPdfFormsParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
onClick: base.handleExecute,
|
||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: unlockPdfFormsOperation,
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: t("unlockPDFForms.results.title", "Unlocked Forms Results"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -330,6 +330,7 @@ export type FileContextAction =
|
||||
| { type: 'PIN_FILE'; payload: { fileId: FileId } }
|
||||
| { type: 'UNPIN_FILE'; payload: { fileId: FileId } }
|
||||
| { type: 'CONSUME_FILES'; payload: { inputFileIds: FileId[]; outputFileRecords: FileRecord[] } }
|
||||
| { type: 'UNDO_CONSUME_FILES'; payload: { inputFileRecords: FileRecord[]; outputFileIds: FileId[] } }
|
||||
|
||||
// UI actions
|
||||
| { type: 'SET_SELECTED_FILES'; payload: { fileIds: FileId[] } }
|
||||
@ -345,9 +346,9 @@ export type FileContextAction =
|
||||
|
||||
export interface FileContextActions {
|
||||
// File management - lightweight actions only
|
||||
addFiles: (files: File[], options?: { insertAfterPageId?: string }) => Promise<FileWithId[]>;
|
||||
addFiles: (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }) => Promise<FileWithId[]>;
|
||||
addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<FileWithId[]>;
|
||||
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => Promise<FileWithId[]>;
|
||||
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise<FileWithId[]>;
|
||||
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise<void>;
|
||||
updateFileRecord: (id: FileId, updates: Partial<FileRecord>) => void;
|
||||
reorderFiles: (orderedFileIds: FileId[]) => void;
|
||||
@ -358,8 +359,9 @@ export interface FileContextActions {
|
||||
pinFile: (file: FileWithId) => void;
|
||||
unpinFile: (file: FileWithId) => void;
|
||||
|
||||
// File consumption (replace unpinned files with outputs) - now returns FileWithId
|
||||
consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise<FileWithId[]>;
|
||||
// File consumption (replace unpinned files with outputs)
|
||||
consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise<FileId[]>;
|
||||
undoConsumeFiles: (inputFiles: File[], inputFileRecords: FileRecord[], outputFileIds: FileId[]) => Promise<void>;
|
||||
// Selection management
|
||||
setSelectedFiles: (fileIds: FileId[]) => void;
|
||||
setSelectedPages: (pageNumbers: number[]) => void;
|
||||
|
Loading…
x
Reference in New Issue
Block a user