mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00

🔄 Dynamic Processing Strategies - Adaptive routing: Same tool uses different backend endpoints based on file analysis - Combined vs separate processing: Intelligently chooses between merge operations and individual file processing - Cross-format workflows: Enable complex conversions like "mixed files → PDF" that other tools can't handle ⚙️ Format-Specific Intelligence Each conversion type gets tailored options: - HTML/ZIP → PDF: Zoom controls (0.1-3.0 increments) with live preview - Email → PDF: Attachment handling, size limits, recipient control - PDF → PDF/A: Digital signature detection with warnings - Images → PDF: Smart combining vs individual file options File Architecture Core Implementation: ├── Convert.tsx # Main stepped workflow UI ├── ConvertSettings.tsx # Centralized settings with smart detection ├── GroupedFormatDropdown.tsx # Enhanced format selector with grouping ├── useConvertParameters.ts # Smart detection & parameter management ├── useConvertOperation.ts # Multi-strategy processing logic └── Settings Components: ├── ConvertFromWebSettings.tsx # HTML zoom controls ├── ConvertFromEmailSettings.tsx # Email attachment options ├── ConvertToPdfaSettings.tsx # PDF/A with signature detection ├── ConvertFromImageSettings.tsx # Image PDF options └── ConvertToImageSettings.tsx # PDF to image options Utility Layer Utils & Services: ├── convertUtils.ts # Format detection & endpoint routing ├── fileResponseUtils.ts # Generic API response handling └── setupTests.ts # Enhanced test environment with crypto mocks Testing & Quality Comprehensive Test Coverage Test Suite: ├── useConvertParameters.test.ts # Parameter logic & smart detection ├── useConvertParametersAutoDetection.test.ts # File type analysis ├── ConvertIntegration.test.tsx # End-to-end conversion workflows ├── ConvertSmartDetectionIntegration.test.tsx # Mixed file scenarios ├── ConvertE2E.spec.ts # Playwright browser tests ├── convertUtils.test.ts # Utility function validation └── fileResponseUtils.test.ts # API response handling Advanced Test Features - Crypto API mocking: Proper test environment for file hashing - File.arrayBuffer() polyfills: Complete browser API simulation - Multi-file scenario testing: Complex batch processing validation - CI/CD integration: Vitest runs in GitHub Actions with proper artifacts --------- Co-authored-by: Connor Yoh <connor@stirlingpdf.com> Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
156 lines
5.1 KiB
TypeScript
156 lines
5.1 KiB
TypeScript
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
|
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
|
import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
|
|
import ApiIcon from "@mui/icons-material/Api";
|
|
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
|
import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool";
|
|
|
|
|
|
// Add entry here with maxFiles, endpoints, and lazy component
|
|
const toolDefinitions: Record<string, ToolDefinition> = {
|
|
split: {
|
|
id: "split",
|
|
icon: <ContentCutIcon />,
|
|
component: React.lazy(() => import("../tools/Split")),
|
|
maxFiles: 1,
|
|
category: "manipulation",
|
|
description: "Split PDF files into smaller parts",
|
|
endpoints: ["split-pages", "split-pdf-by-sections", "split-by-size-or-count", "split-pdf-by-chapters"]
|
|
},
|
|
compress: {
|
|
id: "compress",
|
|
icon: <ZoomInMapIcon />,
|
|
component: React.lazy(() => import("../tools/Compress")),
|
|
maxFiles: -1,
|
|
category: "optimization",
|
|
description: "Reduce PDF file size",
|
|
endpoints: ["compress-pdf"]
|
|
},
|
|
convert: {
|
|
id: "convert",
|
|
icon: <SwapHorizIcon />,
|
|
component: React.lazy(() => import("../tools/Convert")),
|
|
maxFiles: -1,
|
|
category: "manipulation",
|
|
description: "Change to and from PDF and other formats",
|
|
endpoints: ["pdf-to-img", "img-to-pdf", "pdf-to-word", "pdf-to-presentation", "pdf-to-text", "pdf-to-html", "pdf-to-xml", "html-to-pdf", "markdown-to-pdf", "file-to-pdf"],
|
|
supportedFormats: [
|
|
// Microsoft Office
|
|
"doc", "docx", "dot", "dotx", "csv", "xls", "xlsx", "xlt", "xltx", "slk", "dif", "ppt", "pptx",
|
|
// OpenDocument
|
|
"odt", "ott", "ods", "ots", "odp", "otp", "odg", "otg",
|
|
// Text formats
|
|
"txt", "text", "xml", "rtf", "html", "lwp", "md",
|
|
// Images
|
|
"bmp", "gif", "jpeg", "jpg", "png", "tif", "tiff", "pbm", "pgm", "ppm", "ras", "xbm", "xpm", "svg", "svm", "wmf", "webp",
|
|
// StarOffice
|
|
"sda", "sdc", "sdd", "sdw", "stc", "std", "sti", "stw", "sxd", "sxg", "sxi", "sxw",
|
|
// Email formats
|
|
"eml",
|
|
// Archive formats
|
|
"zip",
|
|
// Other
|
|
"dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf"
|
|
]
|
|
},
|
|
swagger: {
|
|
id: "swagger",
|
|
icon: <ApiIcon />,
|
|
component: React.lazy(() => import("../tools/SwaggerUI")),
|
|
maxFiles: 0,
|
|
category: "utility",
|
|
description: "Open API documentation",
|
|
endpoints: ["swagger-ui"]
|
|
},
|
|
ocr: {
|
|
id: "ocr",
|
|
icon: <span className="material-symbols-rounded font-size-20">
|
|
quick_reference_all
|
|
</span>,
|
|
component: React.lazy(() => import("../tools/OCR")),
|
|
maxFiles: -1,
|
|
category: "utility",
|
|
description: "Extract text from images using OCR",
|
|
endpoints: ["ocr-pdf"]
|
|
},
|
|
|
|
};
|
|
|
|
interface ToolManagementResult {
|
|
selectedToolKey: string | null;
|
|
selectedTool: Tool | null;
|
|
toolSelectedFileIds: string[];
|
|
toolRegistry: ToolRegistry;
|
|
selectTool: (toolKey: string) => void;
|
|
clearToolSelection: () => void;
|
|
setToolSelectedFileIds: (fileIds: string[]) => void;
|
|
}
|
|
|
|
export const useToolManagement = (): ToolManagementResult => {
|
|
const { t } = useTranslation();
|
|
|
|
const [selectedToolKey, setSelectedToolKey] = useState<string | null>(null);
|
|
const [toolSelectedFileIds, setToolSelectedFileIds] = useState<string[]>([]);
|
|
|
|
const allEndpoints = Array.from(new Set(
|
|
Object.values(toolDefinitions).flatMap(tool => tool.endpoints || [])
|
|
));
|
|
const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
|
|
|
|
const isToolAvailable = useCallback((toolKey: string): boolean => {
|
|
if (endpointsLoading) return true;
|
|
const tool = toolDefinitions[toolKey];
|
|
if (!tool?.endpoints) return true;
|
|
return tool.endpoints.some(endpoint => endpointStatus[endpoint] === true);
|
|
}, [endpointsLoading, endpointStatus]);
|
|
|
|
const toolRegistry: ToolRegistry = useMemo(() => {
|
|
const availableTools: ToolRegistry = {};
|
|
Object.keys(toolDefinitions).forEach(toolKey => {
|
|
if (isToolAvailable(toolKey)) {
|
|
const toolDef = toolDefinitions[toolKey];
|
|
availableTools[toolKey] = {
|
|
...toolDef,
|
|
name: t(`home.${toolKey}.title`, toolKey.charAt(0).toUpperCase() + toolKey.slice(1))
|
|
};
|
|
}
|
|
});
|
|
return availableTools;
|
|
}, [t, isToolAvailable]);
|
|
|
|
useEffect(() => {
|
|
if (!endpointsLoading && selectedToolKey && !toolRegistry[selectedToolKey]) {
|
|
const firstAvailableTool = Object.keys(toolRegistry)[0];
|
|
if (firstAvailableTool) {
|
|
setSelectedToolKey(firstAvailableTool);
|
|
} else {
|
|
setSelectedToolKey(null);
|
|
}
|
|
}
|
|
}, [endpointsLoading, selectedToolKey, toolRegistry]);
|
|
|
|
const selectTool = useCallback((toolKey: string) => {
|
|
setSelectedToolKey(toolKey);
|
|
}, []);
|
|
|
|
const clearToolSelection = useCallback(() => {
|
|
setSelectedToolKey(null);
|
|
}, []);
|
|
|
|
const selectedTool = selectedToolKey ? toolRegistry[selectedToolKey] : null;
|
|
|
|
return {
|
|
selectedToolKey,
|
|
selectedTool,
|
|
toolSelectedFileIds,
|
|
toolRegistry,
|
|
|
|
selectTool,
|
|
clearToolSelection,
|
|
setToolSelectedFileIds,
|
|
|
|
};
|
|
};
|