From c3b604791a2abcc96dedf3574869b96664b358c2 Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Thu, 25 Sep 2025 09:54:25 +0200 Subject: [PATCH] Refactor refs to use RefObject and improve type safety Updated various components, hooks, and context files to use React.RefObject instead of React.Ref for improved type safety and consistency. Enhanced type definitions for automation parameters, fixed minor bugs, and improved error handling in tool operations and automation execution. Also updated utility functions and test files to align with these changes. --- .../components/pageEditor/DragDropGrid.tsx | 2 +- .../components/pageEditor/PageThumbnail.tsx | 18 ++--- .../tools/shared/createToolFlow.tsx | 52 +++++++++++++-- frontend/src/contexts/file/fileActions.ts | 16 ++--- frontend/src/contexts/file/fileSelectors.ts | 8 +-- frontend/src/contexts/file/lifecycle.ts | 20 +++--- .../tools/automate/useSuggestedAutomations.ts | 4 +- .../src/hooks/tools/shared/useToolApiCalls.ts | 8 ++- frontend/src/hooks/useDocumentMeta.ts | 8 +-- frontend/src/hooks/useIsOverflowing.ts | 2 +- frontend/src/hooks/useTooltipPosition.ts | 4 +- frontend/src/services/automationStorage.ts | 6 +- frontend/src/tools/Automate.tsx | 65 ++++++++++++------- frontend/src/tools/RemovePages.tsx | 6 +- frontend/src/tools/Rotate.tsx | 6 +- frontend/src/types/automation.ts | 56 ++++++++++++++-- frontend/src/types/sidebar.ts | 4 +- frontend/src/utils/automationExecutor.ts | 45 +++++++++---- frontend/src/utils/fileHash.ts | 2 +- frontend/src/utils/fileResponseUtils.test.ts | 50 +++++++------- frontend/src/utils/sidebarUtils.ts | 6 +- 21 files changed, 266 insertions(+), 122 deletions(-) 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