Enforce type checking in CI (#4126)

# Description of Changes
Currently, the `tsconfig.json` file enforces strict type checking, but
nothing in CI checks that the code is actually correctly typed. [Vite
only transpiles TypeScript
code](https://vite.dev/guide/features.html#transpile-only) so doesn't
ensure that the TS code we're running is correct.

This PR adds running of the type checker to CI and fixes the type errors
that have already crept into the codebase.

Note that many of the changes I've made to 'fix the types' are just
using `any` to disable the type checker because the code is under too
much churn to fix anything properly at the moment. I still think
enabling the type checker now is the best course of action though
because otherwise we'll never be able to fix all of them, and it should
at least help us not break things when adding new code.

Co-authored-by: James <james@crosscourtanalytics.com>
This commit is contained in:
James Brunton 2025-08-11 09:16:16 +01:00 committed by GitHub
parent 507ad1dc61
commit af5a9d1ae1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 1141 additions and 919 deletions

View File

@ -9,7 +9,8 @@
"Bash(find:*)",
"Bash(npm test)",
"Bash(npm test:*)",
"Bash(ls:*)"
"Bash(ls:*)",
"Bash(npx tsc:*)"
],
"deny": []
}

View File

@ -39,6 +39,7 @@
},
"devDependencies": {
"@playwright/test": "^1.40.0",
"@types/node": "^24.2.0",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"@vitejs/plugin-react": "^4.5.0",
@ -2384,6 +2385,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz",
"integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
"dev": true,
"dependencies": {
"undici-types": "~7.10.0"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
@ -7404,6 +7414,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
"dev": true
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",

View File

@ -34,8 +34,8 @@
"web-vitals": "^2.1.4"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"dev": "npx tsc --noEmit && vite",
"build": "npx tsc --noEmit && vite build",
"preview": "vite preview",
"generate-licenses": "node scripts/generate-licenses.js",
"test": "vitest",
@ -65,6 +65,7 @@
},
"devDependencies": {
"@playwright/test": "^1.40.0",
"@types/node": "^24.2.0",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"@vitejs/plugin-react": "^4.5.0",

View File

@ -185,7 +185,7 @@ const FileEditor = ({
id: `file-${Date.now()}-${Math.random()}`,
name: file.name,
pageCount: processedFile?.totalPages || Math.floor(Math.random() * 20) + 1,
thumbnail,
thumbnail: thumbnail || '',
size: file.size,
file,
};
@ -605,11 +605,8 @@ const FileEditor = ({
removeFiles([fileId], false);
// Remove from context selections
setContextSelectedFiles(prev => {
const safePrev = Array.isArray(prev) ? prev : [];
return safePrev.filter(id => id !== fileId);
});
const newSelection = contextSelectedIds.filter(id => id !== fileId);
setContextSelectedFiles(newSelection);
// Mark operation as applied
markOperationApplied(fileName, operationId);
} else {
@ -767,21 +764,21 @@ const FileEditor = ({
) : (
<DragDropGrid
items={files}
selectedItems={localSelectedIds}
selectedItems={localSelectedIds as any /* FIX ME */}
selectionMode={selectionMode}
isAnimating={isAnimating}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onEndZoneDragEnter={handleEndZoneDragEnter}
draggedItem={draggedFile}
dropTarget={dropTarget}
multiItemDrag={multiFileDrag}
dragPosition={dragPosition}
renderItem={(file, index, refs) => (
onDragStart={handleDragStart as any /* FIX ME */}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter as any /* FIX ME */}
onDragLeave={handleDragLeave}
onDrop={handleDrop as any /* FIX ME */}
onEndZoneDragEnter={handleEndZoneDragEnter}
draggedItem={draggedFile as any /* FIX ME */}
dropTarget={dropTarget as any /* FIX ME */}
multiItemDrag={multiFileDrag as any /* FIX ME */}
dragPosition={dragPosition}
renderItem={(file, index, refs) => (
<FileThumbnail
file={file}
index={index}
@ -801,8 +798,6 @@ const FileEditor = ({
onToggleFile={toggleFile}
onDeleteFile={handleDeleteFile}
onViewFile={handleViewFile}
onMergeFromHere={handleMergeFromHere}
onSplitFile={handleSplitFile}
onSetStatus={setStatus}
toolMode={toolMode}
isSupported={isFileSupported(file.name)}
@ -831,7 +826,6 @@ const FileEditor = ({
onClose={() => setShowFilePickerModal(false)}
storedFiles={[]} // FileEditor doesn't have access to stored files, needs to be passed from parent
onSelectFiles={handleLoadFromStorage}
allowMultiple={true}
/>
{status && (

View File

@ -29,7 +29,8 @@ const FileOperationHistory: React.FC<FileOperationHistoryProps> = ({
const { getFileHistory, getAppliedOperations } = useFileContext();
const history = getFileHistory(fileId);
const operations = showOnlyApplied ? getAppliedOperations(fileId) : history?.operations || [];
const allOperations = showOnlyApplied ? getAppliedOperations(fileId) : history?.operations || [];
const operations = allOperations.filter(op => 'fileIds' in op) as FileOperation[];
const formatTimestamp = (timestamp: number) => {
return new Date(timestamp).toLocaleString();
@ -62,7 +63,7 @@ const FileOperationHistory: React.FC<FileOperationHistoryProps> = ({
}
};
const renderOperationDetails = (operation: FileOperation | PageOperation) => {
const renderOperationDetails = (operation: FileOperation) => {
if ('metadata' in operation && operation.metadata) {
const { metadata } = operation;
return (

View File

@ -142,7 +142,7 @@ export default function Workbench() {
{/* Top Controls */}
<TopControls
currentView={currentView}
setCurrentView={setCurrentView}
setCurrentView={setCurrentView as any /* FIX ME */}
selectedToolKey={selectedToolKey}
/>

View File

@ -22,7 +22,7 @@ interface DragDropGridProps<T extends DragDropItem> {
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>) => React.ReactNode;
renderSplitMarker?: (item: T, index: number) => React.ReactNode;
draggedItem: number | null;
dropTarget: number | null;
dropTarget: number | 'end' | null;
multiItemDrag: {pageNumbers: number[], count: number} | null;
dragPosition: {x: number, y: number} | null;
}

View File

@ -345,7 +345,7 @@ const FileThumbnail = ({
onClose={() => setShowHistory(false)}
title={`Operation History - ${file.name}`}
size="lg"
scrollAreaComponent="div"
scrollAreaComponent={'div' as any}
>
<FileOperationHistory
fileId={file.name}

View File

@ -43,7 +43,7 @@ export interface PageEditorProps {
onExportAll: () => void;
exportLoading: boolean;
selectionMode: boolean;
selectedPages: string[];
selectedPages: number[];
closePdf: () => void;
}) => void;
}
@ -149,7 +149,7 @@ const PageEditor = ({
// Drag and drop state
const [draggedPage, setDraggedPage] = useState<number | null>(null);
const [dropTarget, setDropTarget] = useState<number | null>(null);
const [dropTarget, setDropTarget] = useState<number | 'end' | null>(null);
const [multiPageDrag, setMultiPageDrag] = useState<{pageNumbers: number[], count: number} | null>(null);
const [dragPosition, setDragPosition] = useState<{x: number, y: number} | null>(null);
@ -890,7 +890,7 @@ const PageEditor = ({
const errors = pdfExportService.validateExport(mergedPdfDocument, exportPageIds, selectedOnly);
if (errors.length > 0) {
setError(errors.join(', '));
setStatus(errors.join(', '));
return;
}
@ -921,7 +921,7 @@ const PageEditor = ({
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Export failed';
setError(errorMessage);
setStatus(errorMessage);
} finally {
setExportLoading(false);
}
@ -1259,7 +1259,7 @@ const PageEditor = ({
selectedPages={selectedPageNumbers}
selectionMode={selectionMode}
draggedPage={draggedPage}
dropTarget={dropTarget}
dropTarget={dropTarget === 'end' ? null : dropTarget}
movingPage={movingPage}
isAnimating={isAnimating}
pageRefs={refs}

View File

@ -35,7 +35,7 @@ interface PageEditorControlsProps {
// Selection state
selectionMode: boolean;
selectedPages: string[];
selectedPages: number[];
}
const PageEditorControls = ({

View File

@ -7,9 +7,9 @@ import RotateRightIcon from '@mui/icons-material/RotateRight';
import DeleteIcon from '@mui/icons-material/Delete';
import ContentCutIcon from '@mui/icons-material/ContentCut';
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
import { PDFPage, PDFDocument } from '../../../types/pageEditor';
import { RotatePagesCommand, DeletePagesCommand, ToggleSplitCommand } from '../../../commands/pageCommands';
import { Command } from '../../../hooks/useUndoRedo';
import { PDFPage, PDFDocument } from '../../types/pageEditor';
import { RotatePagesCommand, DeletePagesCommand, ToggleSplitCommand } from '../../commands/pageCommands';
import { Command } from '../../hooks/useUndoRedo';
import styles from './PageEditor.module.css';
import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist';
@ -29,7 +29,7 @@ interface PageThumbnailProps {
selectedPages: number[];
selectionMode: boolean;
draggedPage: number | null;
dropTarget: number | null;
dropTarget: number | 'end' | null;
movingPage: number | null;
isAnimating: boolean;
pageRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import { useState } from "react";
import { Card, Stack, Text, Group, Badge, Button, Box, Image, ThemeIcon, ActionIcon, Tooltip } from "@mantine/core";
import { useTranslation } from "react-i18next";
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
@ -9,7 +9,6 @@ import EditIcon from "@mui/icons-material/Edit";
import { FileWithUrl } from "../../types/file";
import { getFileSize, getFileDate } from "../../utils/fileUtils";
import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail";
import { fileStorage } from "../../services/fileStorage";
interface FileCardProps {
file: FileWithUrl;

View File

@ -80,7 +80,7 @@ const FileGrid = ({
{showSearch && (
<TextInput
placeholder={t("fileManager.searchFiles", "Search files...")}
leftSection={<SearchIcon size={16} />}
leftSection={<SearchIcon fontSize="small" />}
value={searchTerm}
onChange={(e) => setSearchTerm(e.currentTarget.value)}
style={{ flexGrow: 1, maxWidth: 300, minWidth: 200 }}
@ -96,7 +96,7 @@ const FileGrid = ({
]}
value={sortBy}
onChange={(value) => setSortBy(value as SortOption)}
leftSection={<SortIcon size={16} />}
leftSection={<SortIcon fontSize="small" />}
style={{ minWidth: 150 }}
/>
)}
@ -130,7 +130,7 @@ const FileGrid = ({
<FileCard
key={fileId + idx}
file={file}
onRemove={onRemove ? () => onRemove(originalIdx) : undefined}
onRemove={onRemove ? () => onRemove(originalIdx) : () => {}}
onDoubleClick={onDoubleClick && supported ? () => onDoubleClick(file) : undefined}
onView={onView && supported ? () => onView(file) : undefined}
onEdit={onEdit && supported ? () => onEdit(file) : undefined}

View File

@ -77,7 +77,7 @@ export default function ToolPanel() {
{/* Tool content */}
<div className="flex-1 min-h-0">
<ToolRenderer
selectedToolKey={selectedToolKey}
selectedToolKey={selectedToolKey || ''}
onPreviewFile={setPreviewFile}
/>
</div>

View File

@ -30,7 +30,7 @@ const ConvertFromImageSettings = ({
})}
data={[
{ value: COLOR_TYPES.COLOR, label: t("convert.color", "Color") },
{ value: COLOR_TYPES.GREYSCALE, label: t("convert.greyscale", "Greyscale") },
{ value: COLOR_TYPES.GRAYSCALE, label: t("convert.grayscale", "Grayscale") },
{ value: COLOR_TYPES.BLACK_WHITE, label: t("convert.blackwhite", "Black & White") },
]}
disabled={disabled}

View File

@ -31,7 +31,6 @@ const ConvertFromWebSettings = ({
min={0.1}
max={3.0}
step={0.1}
precision={1}
disabled={disabled}
data-testid="zoom-level-input"
/>

View File

@ -31,7 +31,7 @@ const ConvertToImageSettings = ({
})}
data={[
{ value: COLOR_TYPES.COLOR, label: t("convert.color", "Color") },
{ value: COLOR_TYPES.GREYSCALE, label: t("convert.greyscale", "Greyscale") },
{ value: COLOR_TYPES.GRAYSCALE, label: t("convert.grayscale", "Grayscale") },
{ value: COLOR_TYPES.BLACK_WHITE, label: t("convert.blackwhite", "Black & White") },
]}
disabled={disabled}

View File

@ -30,7 +30,7 @@ const ConvertToPdfaSettings = ({
<Text size="sm" fw={500}>{t("convert.pdfaOptions", "PDF/A Options")}:</Text>
{hasDigitalSignatures && (
<Alert color="yellow" size="sm">
<Alert color="yellow">
<Text size="sm">
{t("convert.pdfaDigitalSignatureWarning", "The PDF contains a digital signature. This will be removed in the next step.")}
</Text>

View File

@ -1,6 +1,6 @@
import { Stack, TextInput, Select, Checkbox } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { SPLIT_MODES, SPLIT_TYPES, type SplitMode, type SplitType } from '../../../constants/splitConstants';
import { isSplitMode, SPLIT_MODES, SPLIT_TYPES, type SplitMode, type SplitType } from '../../../constants/splitConstants';
export interface SplitParameters {
mode: SplitMode | '';
@ -123,7 +123,7 @@ const SplitSettings = ({
label="Choose split method"
placeholder="Select how to split the PDF"
value={parameters.mode}
onChange={(v) => v && onParameterChange('mode', v)}
onChange={(v) => isSplitMode(v) && onParameterChange('mode', v)}
disabled={disabled}
data={[
{ value: SPLIT_MODES.BY_PAGES, label: t("split.header", "Split by Pages") + " (e.g. 1,3,5-10)" },

View File

@ -137,7 +137,7 @@ export interface ViewerProps {
sidebarsVisible: boolean;
setSidebarsVisible: (v: boolean) => void;
onClose?: () => void;
previewFile?: File; // For preview mode - bypasses context
previewFile: File | null; // For preview mode - bypasses context
}
const Viewer = ({
@ -151,11 +151,6 @@ const Viewer = ({
// Get current file from FileContext
const { getCurrentFile, getCurrentProcessedFile, clearAllFiles, addFiles, activeFiles } = useFileContext();
const currentFile = getCurrentFile();
const processedFile = getCurrentProcessedFile();
// Convert File to FileWithUrl format for viewer
const pdfFile = useFileWithUrl(currentFile);
// Tab management for multiple files
const [activeTab, setActiveTab] = useState<string>("0");

View File

@ -1,7 +1,7 @@
export const COLOR_TYPES = {
COLOR: 'color',
GREYSCALE: 'greyscale',
GRAYSCALE: 'grayscale',
BLACK_WHITE: 'blackwhite'
} as const;

View File

@ -20,3 +20,11 @@ export const ENDPOINTS = {
export type SplitMode = typeof SPLIT_MODES[keyof typeof SPLIT_MODES];
export type SplitType = typeof SPLIT_TYPES[keyof typeof SPLIT_TYPES];
export const isSplitMode = (value: string | null): value is SplitMode => {
return Object.values(SPLIT_MODES).includes(value as SplitMode);
}
export const isSplitType = (value: string | null): value is SplitType => {
return Object.values(SPLIT_TYPES).includes(value as SplitType);
}

View File

@ -461,7 +461,8 @@ export function FileContextProvider({
// Force garbage collection hint
if (typeof window !== 'undefined' && window.gc) {
setTimeout(() => window.gc(), 100);
let gc = window.gc
setTimeout(() => gc(), 100);
}
} catch (error) {
@ -504,7 +505,7 @@ export function FileContextProvider({
// File doesn't have explicit ID, store it with thumbnail
try {
// Generate thumbnail for better recent files experience
const thumbnail = await thumbnailGenerationService.generateThumbnail(file);
const thumbnail = await (thumbnailGenerationService as any /* FIX ME */).generateThumbnail(file);
const storedFile = await fileStorage.storeFile(file, thumbnail);
// Add the ID to the file object
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
@ -597,8 +598,9 @@ export function FileContextProvider({
if (state.currentMode !== mode && state.activeFiles.length > 0) {
if (window.requestIdleCallback && typeof window !== 'undefined' && window.gc) {
let gc = window.gc;
window.requestIdleCallback(() => {
window.gc();
gc();
}, { timeout: 5000 });
}
}
@ -611,8 +613,9 @@ export function FileContextProvider({
if (state.currentView !== view && state.activeFiles.length > 0) {
if (window.requestIdleCallback && typeof window !== 'undefined' && window.gc) {
let gc = window.gc;
window.requestIdleCallback(() => {
window.gc();
gc();
}, { timeout: 5000 });
}
}
@ -724,7 +727,7 @@ export function FileContextProvider({
currentView: state.currentView,
currentTool: state.currentTool,
selectedFileIds: state.selectedFileIds,
selectedPageIds: state.selectedPageIds,
selectedPageNumbers: state.selectedPageNumbers,
viewerConfig: state.viewerConfig,
lastExportConfig: state.lastExportConfig,
timestamp: Date.now()
@ -854,7 +857,7 @@ export function useCurrentFile() {
export function useFileSelection() {
const {
selectedFileIds,
selectedPageIds,
selectedPageNumbers,
setSelectedFiles,
setSelectedPages,
clearSelections
@ -862,7 +865,7 @@ export function useFileSelection() {
return {
selectedFileIds,
selectedPageIds,
selectedPageNumbers,
setSelectedFiles,
setSelectedPages,
clearSelections

View File

@ -10,7 +10,7 @@ interface FileManagerContextValue {
searchTerm: string;
selectedFiles: FileWithUrl[];
filteredFiles: FileWithUrl[];
fileInputRef: React.RefObject<HTMLInputElement>;
fileInputRef: React.RefObject<HTMLInputElement | null>;
// Handlers
onSourceChange: (source: 'recent' | 'local' | 'drive') => void;
@ -85,10 +85,14 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
const handleFileSelect = useCallback((file: FileWithUrl) => {
setSelectedFileIds(prev => {
if (prev.includes(file.id)) {
return prev.filter(id => id !== file.id);
if (file.id) {
if (prev.includes(file.id)) {
return prev.filter(id => id !== file.id);
} else {
return [...prev, file.id];
}
} else {
return [...prev, file.id];
return prev;
}
});
}, []);
@ -138,7 +142,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
};
});
onFilesSelected(fileWithUrls);
onFilesSelected(fileWithUrls as any /* FIX ME */);
await refreshRecentFiles();
onClose();
} catch (error) {

View File

@ -7,7 +7,7 @@ interface FilesModalContextType {
closeFilesModal: () => void;
onFileSelect: (file: File) => void;
onFilesSelect: (files: File[]) => void;
onModalClose: () => void;
onModalClose?: () => void;
setOnModalClose: (callback: () => void) => void;
}

View File

@ -345,7 +345,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
test('should handle malformed file objects', () => {
const { result } = renderHook(() => useConvertParameters());
const malformedFiles = [
const malformedFiles: Array<{name: string}> = [
{ name: 'valid.pdf' },
// @ts-ignore - Testing runtime resilience
{ name: null },

View File

@ -99,7 +99,7 @@ export const useOCROperation = () => {
const ocrConfig: ToolOperationConfig<OCRParameters> = {
operationType: 'ocr',
endpoint: '/api/v1/misc/ocr-pdf',
buildFormData,
buildFormData: buildFormData as any /* FIX ME */,
filePrefix: 'ocr_',
multiFileEndpoint: false, // Process files individually
responseHandler, // use shared flow

View File

@ -1,7 +1,7 @@
import { useCallback, useRef } from 'react';
import axios, { CancelTokenSource } from 'axios';
import { processResponse } from '../../../utils/toolResponseProcessor';
import type { ResponseHandler, ProcessingProgress } from './useToolState';
import { processResponse, ResponseHandler } from '../../../utils/toolResponseProcessor';
import type { ProcessingProgress } from './useToolState';
export interface ApiCallsConfig<TParams = void> {
endpoint: string | ((params: TParams) => string);

View File

@ -7,7 +7,7 @@ import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls';
import { useToolResources } from './useToolResources';
import { extractErrorMessage } from '../../../utils/toolErrorHandler';
import { createOperation } from '../../../utils/toolOperationTracker';
import { type ResponseHandler, processResponse } from '../../../utils/toolResponseProcessor';
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
export interface ValidationResult {
valid: boolean;
@ -186,7 +186,7 @@ export const useToolOperation = <TParams = void>(
// Individual file processing - separate API call per file
const apiCallsConfig: ApiCallsConfig<TParams> = {
endpoint: config.endpoint,
buildFormData: (file: File, params: TParams) => (config.buildFormData as (file: File, params: TParams) => FormData)(file, params),
buildFormData: (file: File, params: TParams) => (config.buildFormData as any /* FIX ME */)(file, params),
filePrefix: config.filePrefix,
responseHandler: config.responseHandler
};

View File

@ -40,7 +40,9 @@ export const useToolResources = () => {
for (const file of files) {
try {
const thumbnail = await generateThumbnailForFile(file);
thumbnails.push(thumbnail);
if (thumbnail) {
thumbnails.push(thumbnail);
}
} catch (error) {
console.warn(`Failed to generate thumbnail for ${file.name}:`, error);
thumbnails.push('');

View File

@ -1,6 +1,7 @@
import { useState, useCallback } from 'react';
import { fileStorage } from '../services/fileStorage';
import { FileWithUrl } from '../types/file';
import { createEnhancedFileFromStored } from '../utils/fileUtils';
import { generateThumbnailForFile } from '../utils/thumbnailUtils';
export const useFileManager = () => {
@ -42,7 +43,7 @@ export const useFileManager = () => {
try {
const files = await fileStorage.getAllFiles();
const sortedFiles = files.sort((a, b) => (b.lastModified || 0) - (a.lastModified || 0));
return sortedFiles;
return sortedFiles.map(file => createEnhancedFileFromStored(file));
} catch (error) {
console.error('Failed to load recent files:', error);
return [];

View File

@ -4,7 +4,7 @@ import { useMemo } from 'react';
* Hook to convert a File object to { file: File; url: string } format
* Creates blob URL on-demand and handles cleanup
*/
export function useFileWithUrl(file: File | null): { file: File; url: string } | null {
export function useFileWithUrl(file: File | Blob | null): { file: File | Blob; url: string } | null {
return useMemo(() => {
if (!file) return null;

View File

@ -61,9 +61,9 @@ export function useIndexedDBThumbnail(file: FileWithUrl | undefined | null): {
type: storedFile.type,
lastModified: storedFile.lastModified
});
} else if (file.file) {
} else if ((file as any /* Fix me */).file) {
// For FileWithUrl objects that have a File object
fileObject = file.file;
fileObject = (file as any /* Fix me */).file;
} else if (file.id) {
// Fallback: try to get from IndexedDB even if storedInIndexedDB flag is missing
const storedFile = await fileStorage.getFile(file.id);

View File

@ -44,7 +44,7 @@ function HomePageContent() {
ref={quickAccessRef} />
<ToolPanel />
<Workbench />
<FileManager selectedTool={selectedTool} />
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
</Group>
);
}
@ -53,7 +53,7 @@ export default function HomePage() {
const { setCurrentView } = useFileContext();
return (
<FileSelectionProvider>
<ToolWorkflowProvider onViewChange={setCurrentView}>
<ToolWorkflowProvider onViewChange={setCurrentView as any /* FIX ME */}>
<SidebarProvider>
<HomePageContent />
</SidebarProvider>

View File

@ -520,7 +520,8 @@ export class EnhancedPDFProcessingService {
// Force memory cleanup hint
if (typeof window !== 'undefined' && window.gc) {
setTimeout(() => window.gc(), 100);
let gc = window.gc;
setTimeout(() => gc(), 100);
}
}

View File

@ -73,7 +73,7 @@ export class FileAnalyzer {
}).promise;
const pageCount = pdf.numPages;
const isEncrypted = pdf.isEncrypted;
const isEncrypted = (pdf as any).isEncrypted;
// Clean up
pdf.destroy();

View File

@ -389,7 +389,9 @@ class FileStorageService {
db.close();
} catch (error) {
console.log(`Version ${version} not accessible:`, error.message);
if (error instanceof Error) {
console.log(`Version ${version} not accessible:`, error.message);
}
}
}
}

View File

@ -48,7 +48,8 @@ export class PDFProcessingService {
fileName: file.name,
status: 'processing',
progress: 0,
startedAt: Date.now()
startedAt: Date.now(),
strategy: 'immediate_full'
};
this.processing.set(fileKey, state);
@ -79,7 +80,7 @@ export class PDFProcessingService {
} catch (error) {
console.error('Processing failed for', file.name, ':', error);
state.status = 'error';
state.error = error instanceof Error ? error.message : 'Unknown error';
state.error = (error instanceof Error ? error.message : 'Unknown error') as any;
this.notifyListeners();
// Remove failed processing after delay

View File

@ -1,4 +1,17 @@
import JSZip from 'jszip';
import JSZip, { JSZipObject } from 'jszip';
// Undocumented interface in JSZip for JSZipObject._data
interface CompressedObject {
compressedSize: number;
uncompressedSize: number;
crc32: number;
compression: object;
compressedContent: string|ArrayBuffer|Uint8Array|Buffer;
}
const getData = (zipEntry: JSZipObject): CompressedObject | undefined => {
return (zipEntry as any)._data as CompressedObject;
}
export interface ZipExtractionResult {
success: boolean;
@ -68,7 +81,7 @@ export class ZipFileService {
}
fileCount++;
const uncompressedSize = zipEntry._data?.uncompressedSize || 0;
const uncompressedSize = getData(zipEntry)?.uncompressedSize || 0;
totalSize += uncompressedSize;
// Check if file is a PDF
@ -187,7 +200,7 @@ export class ZipFileService {
const content = await zipEntry.async('uint8array');
// Create File object
const extractedFile = new File([content], this.sanitizeFilename(filename), {
const extractedFile = new File([content as any], this.sanitizeFilename(filename), {
type: 'application/pdf',
lastModified: zipEntry.date?.getTime() || Date.now()
});
@ -312,7 +325,7 @@ export class ZipFileService {
// Check if any files are encrypted
for (const [filename, zipEntry] of Object.entries(zip.files)) {
if (zipEntry.options?.compression === 'STORE' && zipEntry._data?.compressedSize === 0) {
if (zipEntry.options?.compression === 'STORE' && getData(zipEntry)?.compressedSize === 0) {
// This might indicate encryption, but JSZip doesn't provide direct encryption detection
// We'll handle this in the extraction phase
}

View File

@ -75,7 +75,7 @@ Object.defineProperty(globalThis, 'crypto', {
}
return array;
}),
} as Crypto,
} as unknown as Crypto,
writable: true,
configurable: true,
});

View File

@ -135,7 +135,7 @@ async function uploadFileViaModal(page: Page, filePath: string) {
await page.click('[data-testid="files-button"]');
// Wait for the modal to open
await page.waitForSelector('.mantine-Modal-overlay', { state: 'visible' }, { timeout: 5000 });
await page.waitForSelector('.mantine-Modal-overlay', { state: 'visible', timeout: 5000 });
//await page.waitForSelector('[data-testid="file-upload-modal"]', { timeout: 5000 });
// Upload the file through the modal's file input
@ -318,7 +318,13 @@ test.describe('Convert Tool E2E Tests', () => {
// Generate a test for each potentially available conversion
// We'll discover all possible conversions and then skip unavailable ones at runtime
test('PDF to PNG conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/pdf/img', fromFormat: 'pdf', toFormat: 'png' };
const conversion: ConversionEndpoint = {
endpoint: '/api/v1/convert/pdf/img',
fromFormat: 'pdf',
toFormat: 'png',
description: '',
apiPath: ''
};
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
@ -329,7 +335,13 @@ test.describe('Convert Tool E2E Tests', () => {
});
test('PDF to DOCX conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/pdf/word', fromFormat: 'pdf', toFormat: 'docx' };
const conversion: ConversionEndpoint = {
endpoint: '/api/v1/convert/pdf/word',
fromFormat: 'pdf',
toFormat: 'docx',
description: '',
apiPath: ''
};
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
@ -340,7 +352,13 @@ test.describe('Convert Tool E2E Tests', () => {
});
test('DOCX to PDF conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/file/pdf', fromFormat: 'docx', toFormat: 'pdf' };
const conversion: ConversionEndpoint = {
endpoint: '/api/v1/convert/file/pdf',
fromFormat: 'docx',
toFormat: 'pdf',
description: '',
apiPath: ''
};
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
@ -351,7 +369,13 @@ test.describe('Convert Tool E2E Tests', () => {
});
test('Image to PDF conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/img/pdf', fromFormat: 'png', toFormat: 'pdf' };
const conversion: ConversionEndpoint = {
endpoint: '/api/v1/convert/img/pdf',
fromFormat: 'png',
toFormat: 'pdf',
description: '',
apiPath: ''
};
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
@ -362,7 +386,13 @@ test.describe('Convert Tool E2E Tests', () => {
});
test('PDF to TXT conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/pdf/text', fromFormat: 'pdf', toFormat: 'txt' };
const conversion: ConversionEndpoint = {
endpoint: '/api/v1/convert/pdf/text',
fromFormat: 'pdf',
toFormat: 'txt',
description: '',
apiPath: ''
};
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
@ -373,7 +403,13 @@ test.describe('Convert Tool E2E Tests', () => {
});
test('PDF to HTML conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/pdf/html', fromFormat: 'pdf', toFormat: 'html' };
const conversion: ConversionEndpoint = {
endpoint: '/api/v1/convert/pdf/html',
fromFormat: 'pdf',
toFormat: 'html',
description: '',
apiPath: ''
};
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
@ -384,7 +420,13 @@ test.describe('Convert Tool E2E Tests', () => {
});
test('PDF to XML conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/pdf/xml', fromFormat: 'pdf', toFormat: 'xml' };
const conversion: ConversionEndpoint = {
endpoint: '/api/v1/convert/pdf/xml',
fromFormat: 'pdf',
toFormat: 'xml',
description: '',
apiPath: ''
};
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
@ -395,7 +437,13 @@ test.describe('Convert Tool E2E Tests', () => {
});
test('PDF to CSV conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/pdf/csv', fromFormat: 'pdf', toFormat: 'csv' };
const conversion: ConversionEndpoint = {
endpoint: '/api/v1/convert/pdf/csv',
fromFormat: 'pdf',
toFormat: 'csv',
description: '',
apiPath: ''
};
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
@ -406,7 +454,13 @@ test.describe('Convert Tool E2E Tests', () => {
});
test('PDF to PDFA conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/pdf/pdfa', fromFormat: 'pdf', toFormat: 'pdfa' };
const conversion: ConversionEndpoint = {
endpoint: '/api/v1/convert/pdf/pdfa',
fromFormat: 'pdf',
toFormat: 'pdfa',
description: '',
apiPath: ''
};
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);

View File

@ -10,7 +10,7 @@
*/
import React from 'react';
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
import { describe, test, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import { useConvertOperation } from '../../hooks/tools/convert/useConvertOperation';
import { ConvertParameters } from '../../hooks/tools/convert/useConvertParameters';
@ -85,7 +85,7 @@ describe('Convert Tool Integration Tests', () => {
test('should make correct API call for PDF to PNG conversion', async () => {
const mockBlob = new Blob(['fake-image-data'], { type: 'image/png' });
mockedAxios.post.mockResolvedValueOnce({
(mockedAxios.post as Mock).mockResolvedValueOnce({
data: mockBlob,
status: 200,
statusText: 'OK'
@ -108,7 +108,19 @@ describe('Convert Tool Integration Tests', () => {
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
smartDetectionType: 'none',
htmlOptions: {
zoomLevel: 0
},
emailOptions: {
includeAttachments: false,
maxAttachmentSizeMB: 0,
downloadHtml: false,
includeAllRecipients: false
},
pdfaOptions: {
outputFormat: ''
}
};
await act(async () => {
@ -123,7 +135,7 @@ describe('Convert Tool Integration Tests', () => {
);
// Verify FormData contains correct parameters
const formDataCall = mockedAxios.post.mock.calls[0][1] as FormData;
const formDataCall = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
expect(formDataCall.get('imageFormat')).toBe('png');
expect(formDataCall.get('colorType')).toBe('color');
expect(formDataCall.get('dpi')).toBe('300');
@ -138,7 +150,7 @@ describe('Convert Tool Integration Tests', () => {
test('should handle API error responses correctly', async () => {
const errorMessage = 'Invalid file format';
mockedAxios.post.mockRejectedValueOnce({
(mockedAxios.post as Mock).mockRejectedValueOnce({
response: {
status: 400,
data: errorMessage
@ -163,7 +175,19 @@ describe('Convert Tool Integration Tests', () => {
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
smartDetectionType: 'none',
htmlOptions: {
zoomLevel: 0
},
emailOptions: {
includeAttachments: false,
maxAttachmentSizeMB: 0,
downloadHtml: false,
includeAllRecipients: false
},
pdfaOptions: {
outputFormat: ''
}
};
await act(async () => {
@ -177,7 +201,7 @@ describe('Convert Tool Integration Tests', () => {
});
test('should handle network errors gracefully', async () => {
mockedAxios.post.mockRejectedValueOnce(new Error('Network error'));
(mockedAxios.post as Mock).mockRejectedValueOnce(new Error('Network error'));
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
@ -196,7 +220,19 @@ describe('Convert Tool Integration Tests', () => {
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
smartDetectionType: 'none',
htmlOptions: {
zoomLevel: 0
},
emailOptions: {
includeAttachments: false,
maxAttachmentSizeMB: 0,
downloadHtml: false,
includeAllRecipients: false
},
pdfaOptions: {
outputFormat: ''
}
};
await act(async () => {
@ -212,7 +248,7 @@ describe('Convert Tool Integration Tests', () => {
test('should correctly map image conversion parameters to API call', async () => {
const mockBlob = new Blob(['fake-data'], { type: 'image/jpeg' });
mockedAxios.post.mockResolvedValueOnce({
(mockedAxios.post as Mock).mockResolvedValueOnce({
data: mockBlob,
status: 200,
headers: {
@ -229,7 +265,6 @@ describe('Convert Tool Integration Tests', () => {
const parameters: ConvertParameters = {
fromExtension: 'pdf',
toExtension: 'jpg',
pageNumbers: 'all',
imageOptions: {
colorType: 'grayscale',
dpi: 150,
@ -239,7 +274,19 @@ describe('Convert Tool Integration Tests', () => {
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
smartDetectionType: 'none',
htmlOptions: {
zoomLevel: 0
},
emailOptions: {
includeAttachments: false,
maxAttachmentSizeMB: 0,
downloadHtml: false,
includeAllRecipients: false
},
pdfaOptions: {
outputFormat: ''
}
};
await act(async () => {
@ -247,7 +294,7 @@ describe('Convert Tool Integration Tests', () => {
});
// Verify integration: hook parameters → FormData → axios call → hook state
const formDataCall = mockedAxios.post.mock.calls[0][1] as FormData;
const formDataCall = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
expect(formDataCall.get('imageFormat')).toBe('jpg');
expect(formDataCall.get('colorType')).toBe('grayscale');
expect(formDataCall.get('dpi')).toBe('150');
@ -262,7 +309,7 @@ describe('Convert Tool Integration Tests', () => {
test('should make correct API call for PDF to CSV conversion with simplified workflow', async () => {
const mockBlob = new Blob(['fake-csv-data'], { type: 'text/csv' });
mockedAxios.post.mockResolvedValueOnce({
(mockedAxios.post as Mock).mockResolvedValueOnce({
data: mockBlob,
status: 200,
statusText: 'OK'
@ -285,7 +332,19 @@ describe('Convert Tool Integration Tests', () => {
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
smartDetectionType: 'none',
htmlOptions: {
zoomLevel: 0
},
emailOptions: {
includeAttachments: false,
maxAttachmentSizeMB: 0,
downloadHtml: false,
includeAllRecipients: false
},
pdfaOptions: {
outputFormat: ''
}
};
await act(async () => {
@ -300,7 +359,7 @@ describe('Convert Tool Integration Tests', () => {
);
// Verify FormData contains correct parameters for simplified CSV conversion
const formDataCall = mockedAxios.post.mock.calls[0][1] as FormData;
const formDataCall = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
expect(formDataCall.get('pageNumbers')).toBe('all'); // Always "all" for simplified workflow
expect(formDataCall.get('fileInput')).toBe(testFile);
@ -329,7 +388,19 @@ describe('Convert Tool Integration Tests', () => {
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
smartDetectionType: 'none',
htmlOptions: {
zoomLevel: 0
},
emailOptions: {
includeAttachments: false,
maxAttachmentSizeMB: 0,
downloadHtml: false,
includeAllRecipients: false
},
pdfaOptions: {
outputFormat: ''
}
};
await act(async () => {
@ -348,7 +419,7 @@ describe('Convert Tool Integration Tests', () => {
test('should handle multiple file uploads correctly', async () => {
const mockBlob = new Blob(['zip-content'], { type: 'application/zip' });
mockedAxios.post.mockResolvedValueOnce({ data: mockBlob });
(mockedAxios.post as Mock).mockResolvedValueOnce({ data: mockBlob });
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
@ -369,7 +440,19 @@ describe('Convert Tool Integration Tests', () => {
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
smartDetectionType: 'none',
htmlOptions: {
zoomLevel: 0
},
emailOptions: {
includeAttachments: false,
maxAttachmentSizeMB: 0,
downloadHtml: false,
includeAllRecipients: false
},
pdfaOptions: {
outputFormat: ''
}
};
await act(async () => {
@ -377,14 +460,14 @@ describe('Convert Tool Integration Tests', () => {
});
// Verify both files were uploaded
const calls = mockedAxios.post.mock.calls;
const calls = (mockedAxios.post as Mock).mock.calls;
for (let i = 0; i < calls.length; i++) {
const formData = calls[i][1] as FormData;
const fileInputs = formData.getAll('fileInput');
expect(fileInputs).toHaveLength(1);
expect(fileInputs[0]).toBeInstanceOf(File);
expect(fileInputs[0].name).toBe(files[i].name);
expect((fileInputs[0] as File).name).toBe(files[i].name);
}
});
@ -406,7 +489,19 @@ describe('Convert Tool Integration Tests', () => {
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
smartDetectionType: 'none',
htmlOptions: {
zoomLevel: 0
},
emailOptions: {
includeAttachments: false,
maxAttachmentSizeMB: 0,
downloadHtml: false,
includeAllRecipients: false
},
pdfaOptions: {
outputFormat: ''
}
};
await act(async () => {
@ -421,7 +516,7 @@ describe('Convert Tool Integration Tests', () => {
describe('Error Boundary Integration', () => {
test('should handle corrupted file gracefully', async () => {
mockedAxios.post.mockRejectedValueOnce({
(mockedAxios.post as Mock).mockRejectedValueOnce({
response: {
status: 422,
data: 'Processing failed'
@ -445,7 +540,19 @@ describe('Convert Tool Integration Tests', () => {
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
smartDetectionType: 'none',
htmlOptions: {
zoomLevel: 0
},
emailOptions: {
includeAttachments: false,
maxAttachmentSizeMB: 0,
downloadHtml: false,
includeAllRecipients: false
},
pdfaOptions: {
outputFormat: ''
}
};
await act(async () => {
@ -457,7 +564,7 @@ describe('Convert Tool Integration Tests', () => {
});
test('should handle backend service unavailable', async () => {
mockedAxios.post.mockRejectedValueOnce({
(mockedAxios.post as Mock).mockRejectedValueOnce({
response: {
status: 503,
data: 'Service unavailable'
@ -481,7 +588,19 @@ describe('Convert Tool Integration Tests', () => {
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
smartDetectionType: 'none',
htmlOptions: {
zoomLevel: 0
},
emailOptions: {
includeAttachments: false,
maxAttachmentSizeMB: 0,
downloadHtml: false,
includeAllRecipients: false
},
pdfaOptions: {
outputFormat: ''
}
};
await act(async () => {
@ -497,7 +616,7 @@ describe('Convert Tool Integration Tests', () => {
test('should record operation in FileContext', async () => {
const mockBlob = new Blob(['fake-data'], { type: 'image/png' });
mockedAxios.post.mockResolvedValueOnce({
(mockedAxios.post as Mock).mockResolvedValueOnce({
data: mockBlob,
status: 200,
headers: {
@ -523,7 +642,19 @@ describe('Convert Tool Integration Tests', () => {
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
smartDetectionType: 'none',
htmlOptions: {
zoomLevel: 0
},
emailOptions: {
includeAttachments: false,
maxAttachmentSizeMB: 0,
downloadHtml: false,
includeAllRecipients: false
},
pdfaOptions: {
outputFormat: ''
}
};
await act(async () => {
@ -538,7 +669,7 @@ describe('Convert Tool Integration Tests', () => {
test('should clean up blob URLs on reset', async () => {
const mockBlob = new Blob(['fake-data'], { type: 'image/png' });
mockedAxios.post.mockResolvedValueOnce({
(mockedAxios.post as Mock).mockResolvedValueOnce({
data: mockBlob,
status: 200,
headers: {
@ -564,7 +695,19 @@ describe('Convert Tool Integration Tests', () => {
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
smartDetectionType: 'none',
htmlOptions: {
zoomLevel: 0
},
emailOptions: {
includeAttachments: false,
maxAttachmentSizeMB: 0,
downloadHtml: false,
includeAllRecipients: false
},
pdfaOptions: {
outputFormat: ''
}
};
await act(async () => {

View File

@ -4,7 +4,7 @@
*/
import React from 'react';
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
import { describe, test, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import { useConvertOperation } from '../../hooks/tools/convert/useConvertOperation';
import { useConvertParameters } from '../../hooks/tools/convert/useConvertParameters';
@ -59,7 +59,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
vi.clearAllMocks();
// Mock successful API response
mockedAxios.post.mockResolvedValue({
(mockedAxios.post as Mock).mockResolvedValue({
data: new Blob(['fake converted content'], { type: 'application/pdf' })
});
});
@ -186,7 +186,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
});
// Should send all files in single request
const formData = mockedAxios.post.mock.calls[0][1] as FormData;
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
const files = formData.getAll('fileInput');
expect(files).toHaveLength(3);
});
@ -304,7 +304,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
);
});
const formData = mockedAxios.post.mock.calls[0][1] as FormData;
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
expect(formData.get('zoom')).toBe('1.5');
});
@ -338,7 +338,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
);
});
const formData = mockedAxios.post.mock.calls[0][1] as FormData;
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
expect(formData.get('includeAttachments')).toBe('false');
expect(formData.get('maxAttachmentSizeMB')).toBe('20');
expect(formData.get('downloadHtml')).toBe('true');
@ -372,7 +372,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
);
});
const formData = mockedAxios.post.mock.calls[0][1] as FormData;
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
expect(formData.get('outputFormat')).toBe('pdfa');
expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/pdf/pdfa', expect.any(FormData), {
responseType: 'blob'
@ -416,7 +416,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
);
});
const formData = mockedAxios.post.mock.calls[0][1] as FormData;
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
expect(formData.get('fitOption')).toBe('fitToPage');
expect(formData.get('colorType')).toBe('grayscale');
expect(formData.get('autoRotate')).toBe('false');
@ -470,7 +470,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
});
// Mock one success, one failure
mockedAxios.post
(mockedAxios.post as Mock)
.mockResolvedValueOnce({
data: new Blob(['converted1'], { type: 'application/pdf' })
})

View File

@ -64,14 +64,6 @@ export const mantineTheme = createTheme({
xl: 'var(--shadow-xl)',
},
// Font weights
fontWeights: {
normal: 'var(--font-weight-normal)',
medium: 'var(--font-weight-medium)',
semibold: 'var(--font-weight-semibold)',
bold: 'var(--font-weight-bold)',
},
// Component customizations
components: {
Button: {
@ -83,7 +75,7 @@ export const mantineTheme = createTheme({
},
variants: {
// Custom button variant for PDF tools
pdfTool: (theme) => ({
pdfTool: (theme: any) => ({
root: {
backgroundColor: 'var(--bg-surface)',
border: '1px solid var(--border-default)',
@ -95,7 +87,7 @@ export const mantineTheme = createTheme({
},
}),
},
},
} as any,
Paper: {
styles: {
@ -287,28 +279,4 @@ export const mantineTheme = createTheme({
},
},
},
// Global styles
globalStyles: () => ({
// Ensure smooth color transitions
'*': {
transition: 'background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease',
},
// Custom scrollbar styling
'*::-webkit-scrollbar': {
width: '8px',
height: '8px',
},
'*::-webkit-scrollbar-track': {
backgroundColor: 'var(--bg-muted)',
},
'*::-webkit-scrollbar-thumb': {
backgroundColor: 'var(--border-strong)',
borderRadius: 'var(--radius-md)',
},
'*::-webkit-scrollbar-thumb:hover': {
backgroundColor: 'var(--color-primary-500)',
},
}),
});

View File

@ -5,7 +5,11 @@
import { ProcessedFile } from './processing';
import { PDFDocument, PDFPage, PageOperation } from './pageEditor';
export type ModeType = 'viewer' | 'pageEditor' | 'fileEditor' | 'merge' | 'split' | 'compress' | 'ocr';
export type ModeType = 'viewer' | 'pageEditor' | 'fileEditor' | 'merge' | 'split' | 'compress' | 'ocr' | 'convert' | 'sanitize';
export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';
export type ToolType = 'merge' | 'split' | 'compress' | 'ocr' | 'convert' | 'sanitize';
export type OperationType = 'merge' | 'split' | 'compress' | 'add' | 'remove' | 'replace' | 'convert' | 'upload' | 'ocr';
@ -54,6 +58,8 @@ export interface FileContextState {
// Current navigation state
currentMode: ModeType;
currentView: ViewType;
currentTool: ToolType | null;
// Edit history and state
fileEditHistory: Map<string, FileEditHistory>;
@ -85,13 +91,15 @@ export interface FileContextState {
export interface FileContextActions {
// File management
addFiles: (files: File[]) => Promise<void>;
addFiles: (files: File[]) => Promise<File[]>;
removeFiles: (fileIds: string[], deleteFromStorage?: boolean) => void;
replaceFile: (oldFileId: string, newFile: File) => Promise<void>;
clearAllFiles: () => void;
// Navigation
setCurrentMode: (mode: ModeType) => void;
setCurrentView: (view: ViewType) => void;
setCurrentTool: (tool: ToolType) => void;
// Selection management
setSelectedFiles: (fileIds: string[]) => void;
setSelectedPages: (pageNumbers: number[]) => void;

View File

@ -13,6 +13,7 @@ export interface PDFDocument {
file: File;
pages: PDFPage[];
totalPages: number;
destroy?: () => void;
}
export interface PageOperation {
@ -43,7 +44,7 @@ export interface PageEditorFunctions {
handleRedo: () => void;
canUndo: boolean;
canRedo: boolean;
handleRotate: () => void;
handleRotate: (direction: 'left' | 'right') => void;
handleDelete: () => void;
handleSplit: () => void;
onExportSelected: () => void;

View File

@ -35,6 +35,11 @@ export interface ToolResult {
metadata?: Record<string, any>;
}
export interface ToolConfiguration {
maxFiles: number;
supportedFormats?: string[];
}
export interface Tool {
id: string;
name: string;

View File

@ -49,12 +49,16 @@ export function createEnhancedFileFromStored(storedFile: StoredFile, thumbnail?:
size: storedFile.size,
type: storedFile.type,
lastModified: storedFile.lastModified,
webkitRelativePath: '',
// Lazy-loading File interface methods
arrayBuffer: async () => {
const data = await fileStorage.getFileData(storedFile.id);
if (!data) throw new Error(`File ${storedFile.name} not found in IndexedDB - may have been purged`);
return data;
},
bytes: async () => {
return new Uint8Array();
},
slice: (start?: number, end?: number, contentType?: string) => {
// Return a promise-based slice that loads from IndexedDB
return new Blob([], { type: contentType || storedFile.type });
@ -66,7 +70,7 @@ export function createEnhancedFileFromStored(storedFile: StoredFile, thumbnail?:
const data = await fileStorage.getFileData(storedFile.id);
if (!data) throw new Error(`File ${storedFile.name} not found in IndexedDB - may have been purged`);
return new TextDecoder().decode(data);
}
},
} as FileWithUrl;
return enhancedFile;
@ -93,7 +97,7 @@ export async function loadFilesFromIndexedDB(): Promise<FileWithUrl[]> {
})
.map(storedFile => {
try {
return createEnhancedFileFromStored(storedFile);
return createEnhancedFileFromStored(storedFile as any);
} catch (error) {
console.error('Failed to restore file:', storedFile?.name || 'unknown', error);
return null;

View File

@ -183,13 +183,11 @@ export async function generateThumbnailForFile(file: File): Promise<string | und
return generatePlaceholderThumbnail(file);
}
// Calculate quality scale based on file size
console.log('Generating thumbnail for', file.name);
const scale = calculateScaleFromFileSize(file.size);
console.log(`Using scale ${scale} for ${file.name} (${(file.size / 1024 / 1024).toFixed(1)}MB)`);
try {
console.log('Generating thumbnail for', file.name);
// Calculate quality scale based on file size
const scale = calculateScaleFromFileSize(file.size);
console.log(`Using scale ${scale} for ${file.name} (${(file.size / 1024 / 1024).toFixed(1)}MB)`);
// Only read first 2MB for thumbnail generation to save memory
const chunkSize = 2 * 1024 * 1024; // 2MB
const chunk = file.slice(0, Math.min(chunkSize, file.size));

View File

@ -22,7 +22,7 @@ export const createOperation = <TParams = void>(
parameters: params,
fileSize: selectedFiles.reduce((sum, f) => sum + f.size, 0)
}
};
} as any /* FIX ME*/;
return { operation, operationId, fileId };
};