diff --git a/frontend/src/components/pageEditor/DragDropGrid.tsx b/frontend/src/components/pageEditor/DragDropGrid.tsx index 497876cd7..387242d06 100644 --- a/frontend/src/components/pageEditor/DragDropGrid.tsx +++ b/frontend/src/components/pageEditor/DragDropGrid.tsx @@ -14,7 +14,7 @@ interface DragDropGridProps { selectionMode: boolean; isAnimating: boolean; onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void; - renderItem: (item: T, index: number, refs: React.Ref>) => React.ReactNode; + renderItem: (item: T, index: number, refs: React.RefObject>) => React.ReactNode; renderSplitMarker?: (item: T, index: number) => React.ReactNode; } diff --git a/frontend/src/components/pageEditor/PageThumbnail.tsx b/frontend/src/components/pageEditor/PageThumbnail.tsx index e3e951a9d..a0a46e3cb 100644 --- a/frontend/src/components/pageEditor/PageThumbnail.tsx +++ b/frontend/src/components/pageEditor/PageThumbnail.tsx @@ -23,7 +23,7 @@ interface PageThumbnailProps { selectionMode: boolean; movingPage: number | null; isAnimating: boolean; - pageRefs: React.Ref>; + pageRefs: React.RefObject>; onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void; onTogglePage: (pageId: string) => void; onAnimateReorder: () => void; @@ -65,7 +65,11 @@ const PageThumbnail: React.FC = ({ const [isDragging, setIsDragging] = useState(false); const [isMouseDown, setIsMouseDown] = useState(false); const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null); - const dragElementRef = useRef void } | null>(null); + interface DragElement extends HTMLDivElement { + __dragCleanup?: () => void; + } + + const dragElementRef = useRef(null); const [thumbnailUrl, setThumbnailUrl] = useState(page.thumbnail); const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration(); const { openFilesModal } = useFilesModalContext(); @@ -171,19 +175,15 @@ const PageThumbnail: React.FC = ({ type: 'page', pageNumber: page.pageNumber }), - onDrop: (_) => {} + onDrop: (_) => { /* empty */ } }); dragElementRef.current.__dragCleanup = () => { dragCleanup(); dropCleanup(); }; - } else { - if (pageRefs && 'current' in pageRefs && pageRefs.current) { - pageRefs.current.delete(page.id); - } - if (dragElementRef.current && (dragElementRef.current as any).__dragCleanup) { - dragElementRef.current.__dragCleanup?.(); + if (dragElementRef.current?.__dragCleanup) { + dragElementRef.current.__dragCleanup(); } } }, [page.id, page.pageNumber, pageRefs, selectionMode, selectedPageIds, pdfDocument.pages, onReorderPages]); diff --git a/frontend/src/components/tools/shared/createToolFlow.tsx b/frontend/src/components/tools/shared/createToolFlow.tsx index 564563e94..136cc44c7 100644 --- a/frontend/src/components/tools/shared/createToolFlow.tsx +++ b/frontend/src/components/tools/shared/createToolFlow.tsx @@ -11,6 +11,24 @@ import { ChangeMetadataParameters } from 'src/hooks/tools/changeMetadata/useChan import { OCRParameters } from 'src/hooks/tools/ocr/useOCRParameters'; import { TooltipTip } from 'src/types/tips'; import { RemovePasswordParameters } from 'src/hooks/tools/removePassword/useRemovePasswordParameters'; +import { SanitizeParameters } from 'src/hooks/tools/sanitize/useSanitizeParameters'; +import { RotateParameters } from 'src/hooks/tools/rotate/useRotateParameters'; +import { RemovePagesParameters } from 'src/hooks/tools/removePages/useRemovePagesParameters'; +import { RemoveBlanksParameters } from 'src/hooks/tools/removeBlanks/useRemoveBlanksParameters'; +import { RedactParameters } from 'src/hooks/tools/redact/useRedactParameters'; +import { MergeParameters } from 'src/hooks/tools/merge/useMergeParameters'; +import { CropParameters } from 'src/hooks/tools/crop/useCropParameters'; +import { ConvertParameters } from 'src/hooks/tools/convert/useConvertParameters'; +import { ChangePermissionsParameters } from 'src/hooks/tools/changePermissions/useChangePermissionsParameters'; +import { CertSignParameters } from 'src/hooks/tools/certSign/useCertSignParameters'; +import { BookletImpositionParameters } from 'src/hooks/tools/bookletImposition/useBookletImpositionParameters'; +import { AutoRenameParameters } from 'src/hooks/tools/autoRename/useAutoRenameParameters'; +import { FlattenParameters } from 'src/hooks/tools/flatten/useFlattenParameters'; +import { AutomateParameters } from 'src/types/automation'; +import { AdjustPageScaleParameters } from 'src/hooks/tools/adjustPageScale/useAdjustPageScaleParameters'; +import { AddWatermarkParameters } from 'src/hooks/tools/addWatermark/useAddWatermarkParameters'; +import { AddStampParameters } from '../addStamp/useAddStampParameters'; +import { AddPasswordFullParameters } from 'src/hooks/tools/addPassword/useAddPasswordParameters'; export interface FilesStepConfig { selectedFiles: StirlingFile[]; @@ -47,7 +65,29 @@ export interface ExecuteButtonConfig { export interface ReviewStepConfig { isVisible: boolean; - operation: ToolOperationHook | ToolOperationHook | ToolOperationHook | ToolOperationHook | ToolOperationHook; + operation: ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook | + ToolOperationHook; title: string; onFileClick?: (file: File) => void; onUndo: () => void; @@ -99,7 +139,11 @@ export function createToolFlow(config: ToolFlowConfig) { {/* Execute Button */} {config.executeButton && config.executeButton.isVisible !== false && ( { + if (config.executeButton) { + config.executeButton.onClick().catch(console.error); + } + }} isLoading={config.review.operation.isLoading} disabled={config.executeButton.disabled} loadingText={config.executeButton.loadingText} @@ -109,9 +153,9 @@ export function createToolFlow(config: ToolFlowConfig) { )} {/* Review Step */} - {steps.createReviewStep({ + {steps.createReviewStep({ isVisible: config.review.isVisible, - operation: config.review.operation, + operation: config.review.operation as ToolOperationHook, title: config.review.title, onFileClick: config.review.onFileClick, onUndo: config.review.onUndo diff --git a/frontend/src/contexts/file/fileActions.ts b/frontend/src/contexts/file/fileActions.ts index 8e350ad68..6f29e7497 100644 --- a/frontend/src/contexts/file/fileActions.ts +++ b/frontend/src/contexts/file/fileActions.ts @@ -165,8 +165,8 @@ interface AddFileOptions { */ export async function addFiles( options: AddFileOptions, - stateRef: React.Ref, - filesRef: React.Ref>, + stateRef: React.RefObject, + filesRef: React.RefObject>, dispatch: React.Dispatch, lifecycleManager: FileLifecycleManager, enablePersistence = false @@ -204,7 +204,7 @@ export async function addFiles( let thumbnail: string | undefined; if (processedFileMetadata) { // PDF file - use thumbnail from processedFile metadata - thumbnail = processedFileMetadata.thumbnailUrl; + thumbnail = processedFileMetadata.thumbnailUrl as string | undefined; if (DEBUG) console.log(`📄 Generated PDF metadata for ${file.name}: ${processedFileMetadata.totalPages} pages, thumbnail: SUCCESS`); } else if (!file.type.startsWith('application/pdf')) { // Non-PDF files: simple thumbnail generation, no processedFile metadata @@ -278,7 +278,7 @@ export async function consumeFiles( inputFileIds: FileId[], outputStirlingFiles: StirlingFile[], outputStirlingFileStubs: StirlingFileStub[], - filesRef: React.Ref>, + filesRef: React.RefObject>, dispatch: React.Dispatch ): Promise { if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputStirlingFiles.length} output files with pre-created stubs`); @@ -357,7 +357,7 @@ export async function consumeFiles( async function restoreFilesAndCleanup( filesToRestore: { file: File; record: StirlingFileStub }[], fileIdsToRemove: FileId[], - filesRef: React.Ref>, + filesRef: React.RefObject>, indexedDB?: { deleteFile: (fileId: FileId) => Promise } | null ): Promise { // Remove files from filesRef @@ -406,7 +406,7 @@ export async function undoConsumeFiles( inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[], - filesRef: React.Ref>, + filesRef: React.RefObject>, dispatch: React.Dispatch, indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise; deleteFile: (fileId: FileId) => Promise } | null ): Promise { @@ -468,8 +468,8 @@ export async function undoConsumeFiles( export async function addStirlingFileStubs( stirlingFileStubs: StirlingFileStub[], options: { insertAfterPageId?: string; selectFiles?: boolean } = {}, - stateRef: React.Ref, - filesRef: React.Ref>, + stateRef: React.RefObject, + filesRef: React.RefObject>, dispatch: React.Dispatch, _lifecycleManager: FileLifecycleManager ): Promise { diff --git a/frontend/src/contexts/file/fileSelectors.ts b/frontend/src/contexts/file/fileSelectors.ts index cd26c772a..bb4e7487e 100644 --- a/frontend/src/contexts/file/fileSelectors.ts +++ b/frontend/src/contexts/file/fileSelectors.ts @@ -15,8 +15,8 @@ import { * Create stable selectors using stateRef and filesRef */ export function createFileSelectors( - stateRef: React.Ref, - filesRef: React.Ref> + stateRef: React.RefObject, + filesRef: React.RefObject> ): FileContextSelectors { return { getFile: (id: FileId) => { @@ -125,8 +125,8 @@ export function buildQuickKeySetFromMetadata(metadata: { name: string; size: num * Get primary file (first in list) - commonly used pattern */ export function getPrimaryFile( - stateRef: React.Ref, - filesRef: React.Ref> + stateRef: React.RefObject, + filesRef: React.RefObject> ): { file?: File; record?: StirlingFileStub } { const primaryFileId = stateRef.current.files.ids[0]; if (!primaryFileId) return {}; diff --git a/frontend/src/contexts/file/lifecycle.ts b/frontend/src/contexts/file/lifecycle.ts index 1213173b5..6a3940992 100644 --- a/frontend/src/contexts/file/lifecycle.ts +++ b/frontend/src/contexts/file/lifecycle.ts @@ -16,7 +16,7 @@ export class FileLifecycleManager { private fileGenerations = new Map(); // Generation tokens to prevent stale cleanup constructor( - private filesRef: React.Ref>, + private filesRef: React.RefObject | null>, private dispatch: React.Dispatch ) {} @@ -34,7 +34,7 @@ export class FileLifecycleManager { /** * Clean up resources for a specific file (with stateRef access for complete cleanup) */ - cleanupFile = (fileId: FileId, stateRef?: React.Ref): void => { + cleanupFile = (fileId: FileId, stateRef?: React.RefObject): void => { // Use comprehensive cleanup (same as removeFiles) this.cleanupAllResourcesForFile(fileId, stateRef); @@ -62,13 +62,13 @@ export class FileLifecycleManager { this.fileGenerations.clear(); // Clear files ref - this.filesRef.current.clear(); + this.filesRef.current?.clear(); }; /** * Schedule delayed cleanup for a file with generation token to prevent stale cleanup */ - scheduleCleanup = (fileId: FileId, delay = 30000, stateRef?: React.Ref): void => { + scheduleCleanup = (fileId: FileId, delay = 30000, stateRef?: React.RefObject): void => { // Cancel existing timer const existingTimer = this.cleanupTimers.get(fileId); if (existingTimer) { @@ -101,7 +101,7 @@ export class FileLifecycleManager { /** * Remove a file immediately with complete resource cleanup */ - removeFiles = (fileIds: FileId[], stateRef?: React.Ref): void => { + removeFiles = (fileIds: FileId[], stateRef?: React.RefObject): void => { fileIds.forEach(fileId => { // Clean up all resources for this file this.cleanupAllResourcesForFile(fileId, stateRef); @@ -114,9 +114,9 @@ export class FileLifecycleManager { /** * Complete resource cleanup for a single file */ - private cleanupAllResourcesForFile = (fileId: FileId, stateRef?: React.Ref): void => { + private cleanupAllResourcesForFile = (fileId: FileId, stateRef?: React.RefObject): void => { // Remove from files ref - this.filesRef.current.delete(fileId); + this.filesRef.current?.delete(fileId); // Cancel cleanup timer and generation const timer = this.cleanupTimers.get(fileId); @@ -166,15 +166,15 @@ export class FileLifecycleManager { /** * Update file record with race condition guards */ - updateStirlingFileStub = (fileId: FileId, updates: Partial, stateRef?: React.Ref): void => { + updateStirlingFileStub = (fileId: FileId, updates: Partial, stateRef?: React.RefObject): void => { // Guard against updating removed files (race condition protection) - if (!this.filesRef.current.has(fileId)) { + if (!this.filesRef.current?.has(fileId)) { if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`); return; } // Additional state guard for rare race conditions - if (stateRef && !stateRef.current.files.byId[fileId]) { + if (stateRef && 'current' in stateRef && stateRef.current && !stateRef.current.files.byId[fileId]) { if (DEBUG) console.warn(`🗂️ Attempted to update removed file (state): ${fileId}`); return; } diff --git a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts index 970c14375..3f586a0e5 100644 --- a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts +++ b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts @@ -34,7 +34,7 @@ export function useSuggestedAutomations(): SuggestedAutomation[] { { operation: "ocr", parameters: { - languages: ['eng'], + languages: ['eng'] as (string | null)[], ocrType: 'skip-text', ocrRenderType: 'hocr', additionalOptions: ['clean', 'cleanFinal'], @@ -175,7 +175,7 @@ export function useSuggestedAutomations(): SuggestedAutomation[] { { operation: "ocr", parameters: { - languages: ['eng'], + languages: ['eng'] as (string | null)[], ocrType: 'skip-text', ocrRenderType: 'hocr', additionalOptions: [], diff --git a/frontend/src/hooks/tools/shared/useToolApiCalls.ts b/frontend/src/hooks/tools/shared/useToolApiCalls.ts index ae282ebaf..44b533043 100644 --- a/frontend/src/hooks/tools/shared/useToolApiCalls.ts +++ b/frontend/src/hooks/tools/shared/useToolApiCalls.ts @@ -44,11 +44,15 @@ export const useToolApiCalls = () => { // Forward to shared response processor (uses tool-specific responseHandler if provided) const responseFiles = await processResponse( - response.data, + response.data as Blob, [file], config.filePrefix, config.responseHandler, - config.preserveBackendFilename ? response.headers : undefined + config.preserveBackendFilename + ? Object.fromEntries( + Object.entries(response.headers as Record).map(([key, value]) => [key, value?.toString()]) + ) + : undefined ); processedFiles.push(...responseFiles); diff --git a/frontend/src/hooks/useDocumentMeta.ts b/frontend/src/hooks/useDocumentMeta.ts index 824fd1605..10a7357f1 100644 --- a/frontend/src/hooks/useDocumentMeta.ts +++ b/frontend/src/hooks/useDocumentMeta.ts @@ -36,10 +36,10 @@ export const useDocumentMeta = (meta: MetaOptions) => { let metaElement = document.querySelector(`meta[name="${name}"]`)!; if (!metaElement) { metaElement = document.createElement('meta'); - metaElement.name = name; + (metaElement as HTMLMetaElement).name = name; document.head.appendChild(metaElement); } - metaElement.content = content; + (metaElement as HTMLMetaElement).content = content; }; const updateOrCreateProperty = (property: string, content: string) => { @@ -49,7 +49,7 @@ export const useDocumentMeta = (meta: MetaOptions) => { metaElement.setAttribute('property', property); document.head.appendChild(metaElement); } - metaElement.content = content; + (metaElement as HTMLMetaElement).content = content; }; // Update meta tags @@ -90,7 +90,7 @@ export const useDocumentMeta = (meta: MetaOptions) => { const element = document.querySelector(`meta[property="${property}"]`)!; if (element) { if (originalValue !== null) { - element.content = originalValue; + (element as HTMLMetaElement).content = originalValue; } else { element.remove(); } diff --git a/frontend/src/hooks/useIsOverflowing.ts b/frontend/src/hooks/useIsOverflowing.ts index cd2eb23d9..2a6ecfa60 100644 --- a/frontend/src/hooks/useIsOverflowing.ts +++ b/frontend/src/hooks/useIsOverflowing.ts @@ -32,7 +32,7 @@ import * as React from 'react'; */ -export const useIsOverflowing = (ref: React.Ref, callback?: (isOverflow: boolean) => void) => { +export const useIsOverflowing = (ref: React.RefObject, callback?: (isOverflow: boolean) => void) => { // State to track overflow status const [isOverflow, setIsOverflow] = React.useState(undefined); diff --git a/frontend/src/hooks/useTooltipPosition.ts b/frontend/src/hooks/useTooltipPosition.ts index 749ca7fe4..51ef2e9c3 100644 --- a/frontend/src/hooks/useTooltipPosition.ts +++ b/frontend/src/hooks/useTooltipPosition.ts @@ -60,8 +60,8 @@ export function useTooltipPosition({ sidebarTooltip: boolean; position: Position; gap: number; - triggerRef: React.Ref; - tooltipRef: React.Ref; + triggerRef: React.RefObject; + tooltipRef: React.RefObject; sidebarRefs?: SidebarRefs; sidebarState?: SidebarState; }): PositionState { diff --git a/frontend/src/services/automationStorage.ts b/frontend/src/services/automationStorage.ts index 65944261b..e388df395 100644 --- a/frontend/src/services/automationStorage.ts +++ b/frontend/src/services/automationStorage.ts @@ -173,8 +173,10 @@ class AutomationStorage { const lowerQuery = query.toLowerCase(); return automations.filter(automation => automation.name.toLowerCase().includes(lowerQuery) || - (automation.description?.toLowerCase().includes(lowerQuery)) ?? - automation.operations.some(op => op.operation.toLowerCase().includes(lowerQuery)) + ((automation.description?.toLowerCase().includes(lowerQuery)) ?? false), + automations.some(automation => + automation.operations.some(op => op.operation.toLowerCase().includes(lowerQuery)) + ) ); } } diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx index ba47df4f1..de0b93ef2 100644 --- a/frontend/src/tools/Automate.tsx +++ b/frontend/src/tools/Automate.tsx @@ -4,7 +4,7 @@ import { useFileSelection } from "../contexts/FileContext"; import { useNavigationActions } from "../contexts/NavigationContext"; import { useToolWorkflow } from "../contexts/ToolWorkflowContext"; -import { createToolFlow } from "../components/tools/shared/createToolFlow"; +import { createToolFlow, MiddleStepConfig } from "../components/tools/shared/createToolFlow"; import { createFilesToolStep } from "../components/tools/shared/FilesToolStep"; import AutomationSelection from "../components/tools/automate/AutomationSelection"; import AutomationCreation from "../components/tools/automate/AutomationCreation"; @@ -14,8 +14,9 @@ import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperati import { BaseToolProps } from "../types/tool"; import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry"; import { useSavedAutomations } from "../hooks/tools/automate/useSavedAutomations"; -import { AutomationConfig, AutomationStepData, AutomationMode, AutomationStep } from "../types/automation"; +import { AutomationConfig, AutomationStepData, AutomationMode, AutomationStep, AutomateParameters } from "../types/automation"; import { AUTOMATION_STEPS } from "../constants/automation"; +import { StirlingFile } from "src/types/fileContext"; const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); @@ -146,7 +147,13 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { { + const stirlingFiles = files as StirlingFile[]; // Ensure type compatibility + await automateOperation.executeOperation(params as AutomateParameters, stirlingFiles); + } + }} /> ); @@ -155,11 +162,14 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { } }; - const createStep = (title: string, props: any, content?: React.ReactNode) => ({ - title, - ...props, - content - }); + const createStep = (title: string, props: Record, content?: React.ReactNode): React.ReactElement => { + return ( +
+

{title}

+ {content} +
+ ); + }; // Always create files step to avoid conditional hook calls const filesStep = createFilesToolStep(createStep, { @@ -167,8 +177,10 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { isCollapsed: hasResults, }); - const automationSteps = [ - createStep(t('automate.selection.title', 'Automation Selection'), { + const automationSteps: MiddleStepConfig[] = [ + { + title: t('automate.selection.title', 'Automation Selection'), + content: currentStep === AUTOMATION_STEPS.SELECTION ? renderCurrentStep() : null, isVisible: true, isCollapsed: currentStep !== AUTOMATION_STEPS.SELECTION, onCollapsedClick: () => { @@ -177,26 +189,27 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { setCurrentStep(AUTOMATION_STEPS.SELECTION); setStepData({ step: AUTOMATION_STEPS.SELECTION }); } - }, currentStep === AUTOMATION_STEPS.SELECTION ? renderCurrentStep() : null), - - createStep(stepData.mode === AutomationMode.EDIT - ? t('automate.creation.editTitle', 'Edit Automation') - : t('automate.creation.createTitle', 'Create Automation'), { + }, + { + title: stepData.mode === AutomationMode.EDIT + ? t('automate.creation.editTitle', 'Edit Automation') + : t('automate.creation.createTitle', 'Create Automation'), + content: currentStep === AUTOMATION_STEPS.CREATION ? renderCurrentStep() : null, isVisible: currentStep === AUTOMATION_STEPS.CREATION, isCollapsed: false - }, currentStep === AUTOMATION_STEPS.CREATION ? renderCurrentStep() : null), - - // Files step - only visible during run mode + }, { ...filesStep, + title: t('automate.files.title', 'Files'), + content: null, // Files step content is managed separately isVisible: currentStep === AUTOMATION_STEPS.RUN }, - - // Run step - createStep(t('automate.run.title', 'Run Automation'), { + { + title: t('automate.run.title', 'Run Automation'), + content: currentStep === AUTOMATION_STEPS.RUN ? renderCurrentStep() : null, isVisible: currentStep === AUTOMATION_STEPS.RUN, - isCollapsed: hasResults, - }, currentStep === AUTOMATION_STEPS.RUN ? renderCurrentStep() : null) + isCollapsed: hasResults + } ]; return createToolFlow({ @@ -214,7 +227,11 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { onPreviewFile?.(file); actions.setWorkbench('viewer'); }, - onUndo: handleUndo + onUndo: () => { + handleUndo().catch((error) => { + console.error('Undo operation failed:', error); + }); + } } }); }; diff --git a/frontend/src/tools/RemovePages.tsx b/frontend/src/tools/RemovePages.tsx index 5ae0f6934..506d0a4dd 100644 --- a/frontend/src/tools/RemovePages.tsx +++ b/frontend/src/tools/RemovePages.tsx @@ -54,7 +54,11 @@ const RemovePages = (props: BaseToolProps) => { operation: base.operation, title: t("removePages.results.title", "Pages Removed"), onFileClick: base.handleThumbnailClick, - onUndo: base.handleUndo, + onUndo: () => { + base.handleUndo().catch((error) => { + console.error("Undo operation failed:", error); + }); + }, }, }); }; diff --git a/frontend/src/tools/Rotate.tsx b/frontend/src/tools/Rotate.tsx index 40b75f69a..b5c3c8062 100644 --- a/frontend/src/tools/Rotate.tsx +++ b/frontend/src/tools/Rotate.tsx @@ -50,7 +50,11 @@ const Rotate = (props: BaseToolProps) => { operation: base.operation, title: t("rotate.title", "Rotation Results"), onFileClick: base.handleThumbnailClick, - onUndo: base.handleUndo, + onUndo: () => { + base.handleUndo().catch((error) => { + console.error("Undo operation failed:", error); + }); + }, }, }); }; diff --git a/frontend/src/types/automation.ts b/frontend/src/types/automation.ts index ed11ea291..cd65cb9c8 100644 --- a/frontend/src/types/automation.ts +++ b/frontend/src/types/automation.ts @@ -4,7 +4,7 @@ export interface AutomationOperation { operation: string; - parameters: Record; + parameters: Record; } export interface AutomationConfig { @@ -22,7 +22,7 @@ export interface AutomationTool { operation: string; name: string; configured: boolean; - parameters?: Record; + parameters?: Record; } export type AutomationStep = typeof import('../constants/automation').AUTOMATION_STEPS[keyof typeof import('../constants/automation').AUTOMATION_STEPS]; @@ -47,10 +47,6 @@ export interface AutomationExecutionCallbacks { onStepError?: (stepIndex: number, error: string) => void; } -export interface AutomateParameters extends AutomationExecutionCallbacks { - automationConfig?: AutomationConfig; -} - export enum AutomationMode { CREATE = 'create', EDIT = 'edit', @@ -70,4 +66,50 @@ export interface SuggestedAutomation { // Export the AutomateParameters interface that was previously defined inline export interface AutomateParameters extends AutomationExecutionCallbacks { automationConfig?: AutomationConfig; -} \ No newline at end of file +} + +/** + * Typen für Automations-Funktionalität + */ + +// JSON-ähnlicher Wertetyp: erlaubt Strings, Zahlen, Booleans, null, +// Arrays und verschachtelte Objekte – genau das, was "parameters" benötigt. +export type JsonValue = + | string + | number + | boolean + | null + | JsonValue[] + | { [key: string]: JsonValue }; + +export type JsonObject = Record; + +export interface AutomationOperation { + operation: string; + // Wurde von Record auf JSON erweitert + parameters: Record; +} + +export interface AutomationConfig { + id: string; + name: string; + description?: string; + icon?: string; + operations: AutomationOperation[]; + createdAt: string; + updatedAt: string; +} + +export interface AutomationStepData { + step: AutomationStep; + mode?: AutomationMode; + automation?: AutomationConfig; +} + +export interface ExecutionStep { + id: string; + operation: string; + name: string; + status: 'pending' | 'running' | 'completed' | 'error'; + error?: string; +} diff --git a/frontend/src/types/sidebar.ts b/frontend/src/types/sidebar.ts index d03bb9ec1..b93db61d6 100644 --- a/frontend/src/types/sidebar.ts +++ b/frontend/src/types/sidebar.ts @@ -5,8 +5,8 @@ export interface SidebarState { } export interface SidebarRefs { - quickAccessRef: React.Ref; - toolPanelRef: React.Ref; + quickAccessRef: React.RefObject; + toolPanelRef: React.RefObject; } export interface SidebarInfo { diff --git a/frontend/src/utils/automationExecutor.ts b/frontend/src/utils/automationExecutor.ts index 88860a646..0e1c73791 100644 --- a/frontend/src/utils/automationExecutor.ts +++ b/frontend/src/utils/automationExecutor.ts @@ -62,11 +62,15 @@ export const executeToolOperationWithPrefix = async ( timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT }); - console.log(`📥 Response status: ${response.status}, size: ${response.data.size} bytes`); + if (response.data instanceof Blob) { + console.log(`📥 Response status: ${response.status}, size: ${response.data.size} bytes`); + } else { + console.warn(`📥 Response data is not a Blob, unable to determine size.`); + } // Multi-file responses are typically ZIP files, but may be single files (e.g. split with merge=true) let result; - if (response.data.type === 'application/pdf' || + if ((response.data as Blob).type === 'application/pdf' || (response.headers && response.headers['content-type'] === 'application/pdf')) { // Single PDF response (e.g. split with merge option) - use processResponse to respect preserveBackendFilename const processedFiles = await processResponse( @@ -85,7 +89,11 @@ export const executeToolOperationWithPrefix = async ( }; } else { // ZIP response - result = await AutomationFileProcessor.extractAutomationZipFiles(response.data); + if (response.data instanceof Blob) { + result = await AutomationFileProcessor.extractAutomationZipFiles(response.data); + } else { + throw new Error('Response data is not a Blob, unable to process ZIP files.'); + } } if (result.errors.length > 0) { @@ -123,15 +131,21 @@ export const executeToolOperationWithPrefix = async ( timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT }); - console.log(`📥 Response ${i+1} status: ${response.status}, size: ${response.data.size} bytes`); + if (response.data instanceof Blob) { + console.log(`📥 Response ${i+1} status: ${response.status}, size: ${response.data.size} bytes`); + } else { + console.warn(`📥 Response ${i+1} data is not a Blob, unable to determine size.`); + } // Create result file using processResponse to respect preserveBackendFilename setting const processedFiles = await processResponse( - response.data, + response.data as Blob, [file], filePrefix, undefined, - config.preserveBackendFilename ? response.headers : undefined + config.preserveBackendFilename ? Object.fromEntries( + Object.entries(response.headers || {}).map(([key, value]) => [key, value != null ? String(value) : undefined]) + ) : undefined ); resultFiles.push(...processedFiles); console.log(`✅ Created result file(s): ${processedFiles.map(f => f.name).join(', ')}`); @@ -141,17 +155,26 @@ export const executeToolOperationWithPrefix = async ( return resultFiles; } - } catch (error: any) { + } catch (error: unknown) { console.error(`Tool operation ${operationName} failed:`, error); - throw new Error(`${operationName} operation failed: ${error.response?.data ?? error.message}`); + if (error instanceof Error) { + throw new Error(`${operationName} operation failed: ${error.message}`); + } else { + throw new Error(`${operationName} operation failed: Unknown error`); + } } }; /** * Execute an entire automation sequence */ +interface Automation { + name?: string; + operations: { operation: string; parameters?: Record }[]; +} + export const executeAutomationSequence = async ( - automation: any, + automation: Automation, initialFiles: File[], toolRegistry: ToolRegistry, onStepStart?: (stepIndex: number, operationName: string) => void, @@ -191,9 +214,9 @@ export const executeAutomationSequence = async ( currentFiles = resultFiles; onStepComplete?.(i, resultFiles); - } catch (error: any) { + } catch (error: unknown) { console.error(`❌ Step ${i + 1} failed:`, error); - onStepError?.(i, error.message); + onStepError?.(i, error instanceof Error ? error.message : 'Unknown error'); throw error; } } diff --git a/frontend/src/utils/fileHash.ts b/frontend/src/utils/fileHash.ts index 99b1bdb34..afb091c41 100644 --- a/frontend/src/utils/fileHash.ts +++ b/frontend/src/utils/fileHash.ts @@ -83,7 +83,7 @@ export class FileHasher { offset += chunk.byteLength; } - return combined.buffer; + return Promise.resolve(combined.buffer); } private static async hashArrayBuffer(buffer: ArrayBuffer): Promise { diff --git a/frontend/src/utils/fileResponseUtils.test.ts b/frontend/src/utils/fileResponseUtils.test.ts index 2f16a7c61..341e3dcfc 100644 --- a/frontend/src/utils/fileResponseUtils.test.ts +++ b/frontend/src/utils/fileResponseUtils.test.ts @@ -6,66 +6,66 @@ import { describe, test, expect } from 'vitest'; import { getFilenameFromHeaders, createFileFromApiResponse } from './fileResponseUtils'; describe('fileResponseUtils', () => { - + describe('getFilenameFromHeaders', () => { - + test('should extract filename from content-disposition header', () => { const contentDisposition = 'attachment; filename="document.pdf"'; const filename = getFilenameFromHeaders(contentDisposition); - + expect(filename).toBe('document.pdf'); }); test('should extract filename without quotes', () => { const contentDisposition = 'attachment; filename=document.pdf'; const filename = getFilenameFromHeaders(contentDisposition); - + expect(filename).toBe('document.pdf'); }); test('should handle single quotes', () => { const contentDisposition = "attachment; filename='document.pdf'"; const filename = getFilenameFromHeaders(contentDisposition); - + expect(filename).toBe('document.pdf'); }); test('should return null for malformed header', () => { const contentDisposition = 'attachment; invalid=format'; const filename = getFilenameFromHeaders(contentDisposition); - + expect(filename).toBe(null); }); test('should return null for empty header', () => { const filename = getFilenameFromHeaders(''); - + expect(filename).toBe(null); }); test('should return null for undefined header', () => { const filename = getFilenameFromHeaders(); - + expect(filename).toBe(null); }); test('should handle complex filenames with spaces and special chars', () => { const contentDisposition = 'attachment; filename="My Document (1).pdf"'; const filename = getFilenameFromHeaders(contentDisposition); - + expect(filename).toBe('My Document (1).pdf'); }); test('should handle filename with extension when downloadHtml is enabled', () => { const contentDisposition = 'attachment; filename="email_content.html"'; const filename = getFilenameFromHeaders(contentDisposition); - + expect(filename).toBe('email_content.html'); }); }); describe('createFileFromApiResponse', () => { - + test('should create file using header filename when available', () => { const responseData = new Uint8Array([1, 2, 3, 4]); const headers = { @@ -73,9 +73,9 @@ describe('fileResponseUtils', () => { 'content-disposition': 'attachment; filename="server_filename.pdf"' }; const fallbackFilename = 'fallback.pdf'; - + const file = createFileFromApiResponse(responseData, headers, fallbackFilename); - + expect(file.name).toBe('server_filename.pdf'); expect(file.type).toBe('application/pdf'); expect(file.size).toBe(4); @@ -87,9 +87,9 @@ describe('fileResponseUtils', () => { 'content-type': 'application/pdf' }; const fallbackFilename = 'converted_file.pdf'; - + const file = createFileFromApiResponse(responseData, headers, fallbackFilename); - + expect(file.name).toBe('converted_file.pdf'); expect(file.type).toBe('application/pdf'); }); @@ -101,9 +101,9 @@ describe('fileResponseUtils', () => { 'content-disposition': 'attachment; filename="email_content.html"' }; const fallbackFilename = 'fallback.pdf'; - + const file = createFileFromApiResponse(responseData, headers, fallbackFilename); - + expect(file.name).toBe('email_content.html'); expect(file.type).toBe('text/html'); }); @@ -115,9 +115,9 @@ describe('fileResponseUtils', () => { 'content-disposition': 'attachment; filename="converted_files.zip"' }; const fallbackFilename = 'fallback.pdf'; - + const file = createFileFromApiResponse(responseData, headers, fallbackFilename); - + expect(file.name).toBe('converted_files.zip'); expect(file.type).toBe('application/zip'); }); @@ -126,22 +126,22 @@ describe('fileResponseUtils', () => { const responseData = new Uint8Array([1, 2, 3, 4]); const headers = {}; const fallbackFilename = 'test.bin'; - + const file = createFileFromApiResponse(responseData, headers, fallbackFilename); - + expect(file.name).toBe('test.bin'); expect(file.type).toBe('application/octet-stream'); }); test('should handle null/undefined headers gracefully', () => { const responseData = new Uint8Array([1, 2, 3, 4]); - const headers = null; + const headers = {} as Record; const fallbackFilename = 'test.bin'; - + const file = createFileFromApiResponse(responseData, headers, fallbackFilename); - + expect(file.name).toBe('test.bin'); expect(file.type).toBe('application/octet-stream'); }); }); -}); \ No newline at end of file +}); diff --git a/frontend/src/utils/sidebarUtils.ts b/frontend/src/utils/sidebarUtils.ts index 9dad55f9e..d6aac20f7 100644 --- a/frontend/src/utils/sidebarUtils.ts +++ b/frontend/src/utils/sidebarUtils.ts @@ -1,3 +1,4 @@ +import { RefObject } from 'react'; import { SidebarRefs, SidebarState, SidebarInfo } from '../types/sidebar'; /** @@ -7,7 +8,10 @@ import { SidebarRefs, SidebarState, SidebarInfo } from '../types/sidebar'; * @returns Object containing the sidebar rect and whether the tool panel is active */ export function getSidebarInfo(refs: SidebarRefs, state: SidebarState): SidebarInfo { - const { quickAccessRef, toolPanelRef } = refs; + const { quickAccessRef, toolPanelRef } = refs as { + quickAccessRef: RefObject; + toolPanelRef: RefObject; + }; const { sidebarsVisible, readerMode } = state; // Determine if tool panel should be active based on state