mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 18:45:21 +00:00
pdf-csv simplified
This commit is contained in:
parent
fa7dc1234a
commit
8dff995a1c
@ -304,7 +304,8 @@ const FileEditor = ({
|
||||
});
|
||||
}
|
||||
} else {
|
||||
errors.push(`Unsupported file type: ${file.name} (${file.type})`);
|
||||
console.log(`Adding none PDF file: ${file.name} (${file.type})`);
|
||||
allExtractedFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
@ -681,7 +682,7 @@ const FileEditor = ({
|
||||
|
||||
<Dropzone
|
||||
onDrop={handleFileUpload}
|
||||
accept={["application/pdf", "application/zip", "application/x-zip-compressed"]}
|
||||
accept={["*/*"]}
|
||||
multiple={true}
|
||||
maxSize={2 * 1024 * 1024 * 1024}
|
||||
style={{ display: 'contents' }}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { Stack, Text, Select } from "@mantine/core";
|
||||
import { Stack, Text, Select, Switch } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { COLOR_TYPES } from "../../../constants/convertConstants";
|
||||
import { COLOR_TYPES, FIT_OPTIONS } from "../../../constants/convertConstants";
|
||||
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
||||
|
||||
interface ConvertFromImageSettingsProps {
|
||||
@ -35,6 +35,46 @@ const ConvertFromImageSettings = ({
|
||||
]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Select
|
||||
data-testid="fit-option-select"
|
||||
label={t("convert.fitOption", "Fit Option")}
|
||||
value={parameters.imageOptions.fitOption}
|
||||
onChange={(val) => val && onParameterChange('imageOptions', {
|
||||
...parameters.imageOptions,
|
||||
fitOption: val as typeof FIT_OPTIONS[keyof typeof FIT_OPTIONS]
|
||||
})}
|
||||
data={[
|
||||
{ value: FIT_OPTIONS.MAINTAIN_ASPECT, label: t("convert.maintainAspectRatio", "Maintain Aspect Ratio") },
|
||||
{ value: FIT_OPTIONS.FIT_PAGE, label: t("convert.fitDocumentToPage", "Fit Document to Page") },
|
||||
{ value: FIT_OPTIONS.FILL_PAGE, label: t("convert.fillPage", "Fill Page") },
|
||||
]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
data-testid="auto-rotate-switch"
|
||||
label={t("convert.autoRotate", "Auto Rotate")}
|
||||
description={t("convert.autoRotateDescription", "Automatically rotate images to better fit the PDF page")}
|
||||
checked={parameters.imageOptions.autoRotate}
|
||||
onChange={(event) => onParameterChange('imageOptions', {
|
||||
...parameters.imageOptions,
|
||||
autoRotate: event.currentTarget.checked
|
||||
})}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
data-testid="combine-images-switch"
|
||||
label={t("convert.combineImages", "Combine Images")}
|
||||
description={t("convert.combineImagesDescription", "Combine all images into one PDF, or create separate PDFs for each image")}
|
||||
checked={parameters.imageOptions.combineImages}
|
||||
onChange={(event) => onParameterChange('imageOptions', {
|
||||
...parameters.imageOptions,
|
||||
combineImages: event.currentTarget.checked
|
||||
})}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
@ -1,37 +0,0 @@
|
||||
import React from "react";
|
||||
import { Stack, Text, TextInput } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
||||
|
||||
interface ConvertFromPdfToCsvSettingsProps {
|
||||
parameters: ConvertParameters;
|
||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ConvertFromPdfToCsvSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false
|
||||
}: ConvertFromPdfToCsvSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="sm" data-testid="csv-options-section">
|
||||
<Text size="sm" fw={500} data-testid="csv-options-title">
|
||||
{t("convert.csvOptions", "CSV Options")}:
|
||||
</Text>
|
||||
<TextInput
|
||||
data-testid="page-numbers-input"
|
||||
label={t("convert.pageNumbers", "Page Numbers")}
|
||||
placeholder={t("convert.pageNumbersPlaceholder", "e.g., 1,3,5-9, 2n+1, or 'all'")}
|
||||
description={t("convert.pageNumbersDescription", "Specify pages to extract CSV data from. Supports ranges (e.g., '1,3,5-9'), functions (e.g., '2n+1', '3n'), or 'all' for all pages.")}
|
||||
value={parameters.pageNumbers}
|
||||
onChange={(event) => onParameterChange('pageNumbers', event.currentTarget.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConvertFromPdfToCsvSettings;
|
@ -7,13 +7,13 @@ import { isImageFormat } from "../../../utils/convertUtils";
|
||||
import GroupedFormatDropdown from "./GroupedFormatDropdown";
|
||||
import ConvertToImageSettings from "./ConvertToImageSettings";
|
||||
import ConvertFromImageSettings from "./ConvertFromImageSettings";
|
||||
import ConvertFromPdfToCsvSettings from "./ConvertFromPdfToCsvSettings";
|
||||
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
||||
import {
|
||||
FROM_FORMAT_OPTIONS,
|
||||
EXTENSION_TO_ENDPOINT,
|
||||
COLOR_TYPES,
|
||||
OUTPUT_OPTIONS
|
||||
OUTPUT_OPTIONS,
|
||||
FIT_OPTIONS
|
||||
} from "../../../constants/convertConstants";
|
||||
|
||||
interface ConvertSettingsProps {
|
||||
@ -84,14 +84,24 @@ const ConvertSettings = ({
|
||||
|
||||
const handleFromExtensionChange = (value: string) => {
|
||||
onParameterChange('fromExtension', value);
|
||||
// Reset to extension when from extension changes
|
||||
onParameterChange('toExtension', '');
|
||||
|
||||
// Auto-select target if only one option available
|
||||
const availableToOptions = getAvailableToExtensions(value);
|
||||
const autoTarget = availableToOptions.length === 1 ? availableToOptions[0].value : '';
|
||||
onParameterChange('toExtension', autoTarget);
|
||||
|
||||
// Reset format-specific options
|
||||
onParameterChange('imageOptions', {
|
||||
colorType: COLOR_TYPES.COLOR,
|
||||
dpi: 300,
|
||||
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
||||
fitOption: FIT_OPTIONS.MAINTAIN_ASPECT,
|
||||
autoRotate: true,
|
||||
combineImages: true,
|
||||
});
|
||||
// Disable smart detection when manually changing source format
|
||||
onParameterChange('isSmartDetection', false);
|
||||
onParameterChange('smartDetectionType', 'none');
|
||||
};
|
||||
|
||||
const handleToExtensionChange = (value: string) => {
|
||||
@ -101,13 +111,16 @@ const ConvertSettings = ({
|
||||
colorType: COLOR_TYPES.COLOR,
|
||||
dpi: 300,
|
||||
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
||||
fitOption: FIT_OPTIONS.MAINTAIN_ASPECT,
|
||||
autoRotate: true,
|
||||
combineImages: true,
|
||||
});
|
||||
onParameterChange('pageNumbers', 'all');
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
|
||||
{/* Format Selection */}
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>
|
||||
@ -120,7 +133,7 @@ const ConvertSettings = ({
|
||||
placeholder={t("convert.sourceFormatPlaceholder", "Source format")}
|
||||
options={enhancedFromOptions}
|
||||
onChange={handleFromExtensionChange}
|
||||
disabled={disabled}
|
||||
disabled={disabled || parameters.isSmartDetection}
|
||||
minWidth="21.875rem"
|
||||
/>
|
||||
</Stack>
|
||||
@ -150,6 +163,17 @@ const ConvertSettings = ({
|
||||
/>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
) : parameters.isSmartDetection ? (
|
||||
<GroupedFormatDropdown
|
||||
name="convert-to-dropdown"
|
||||
data-testid="to-format-dropdown"
|
||||
value="pdf"
|
||||
placeholder="PDF"
|
||||
options={[{ value: 'pdf', label: 'PDF', group: 'Document' }]}
|
||||
onChange={() => {}} // No-op since it's disabled
|
||||
disabled={true}
|
||||
minWidth="21.875rem"
|
||||
/>
|
||||
) : (
|
||||
<GroupedFormatDropdown
|
||||
name="convert-to-dropdown"
|
||||
@ -178,7 +202,8 @@ const ConvertSettings = ({
|
||||
|
||||
|
||||
{/* Color options for image to PDF conversion */}
|
||||
{isImageFormat(parameters.fromExtension) && parameters.toExtension === 'pdf' && (
|
||||
{(isImageFormat(parameters.fromExtension) && parameters.toExtension === 'pdf') ||
|
||||
(parameters.isSmartDetection && parameters.smartDetectionType === 'images') ? (
|
||||
<>
|
||||
<Divider />
|
||||
<ConvertFromImageSettings
|
||||
@ -187,7 +212,7 @@ const ConvertSettings = ({
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{/* EML specific options */}
|
||||
{parameters.fromExtension === 'eml' && parameters.toExtension === 'pdf' && (
|
||||
@ -202,17 +227,6 @@ const ConvertSettings = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* CSV specific options */}
|
||||
{parameters.fromExtension === 'pdf' && parameters.toExtension === 'csv' && (
|
||||
<>
|
||||
<Divider />
|
||||
<ConvertFromPdfToCsvSettings
|
||||
parameters={parameters}
|
||||
onParameterChange={onParameterChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
@ -86,7 +86,9 @@ const GroupedFormatDropdown = ({
|
||||
: theme.white,
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
width: '100%',
|
||||
color: colorScheme === 'dark' ? theme.colors.dark[0] : theme.colors.dark[9]
|
||||
color: disabled
|
||||
? colorScheme === 'dark' ? theme.colors.dark[1] : theme.colors.dark[7]
|
||||
: colorScheme === 'dark' ? theme.colors.dark[0] : theme.colors.dark[9]
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between">
|
||||
|
@ -10,6 +10,12 @@ export const OUTPUT_OPTIONS = {
|
||||
MULTIPLE: 'multiple'
|
||||
} as const;
|
||||
|
||||
export const FIT_OPTIONS = {
|
||||
FIT_PAGE: 'fitDocumentToPage',
|
||||
MAINTAIN_ASPECT: 'maintainAspectRatio',
|
||||
FILL_PAGE: 'fillPage'
|
||||
} as const;
|
||||
|
||||
|
||||
export const CONVERSION_ENDPOINTS = {
|
||||
'office-pdf': '/api/v1/convert/file/pdf',
|
||||
@ -48,6 +54,8 @@ export const ENDPOINT_NAMES = {
|
||||
|
||||
// Grouped file extensions for dropdowns
|
||||
export const FROM_FORMAT_OPTIONS = [
|
||||
{ value: 'any', label: 'Any', group: 'Multiple Files' },
|
||||
{ value: 'image', label: 'Images', group: 'Multiple Files' },
|
||||
{ value: 'pdf', label: 'PDF', group: 'Document' },
|
||||
{ value: 'docx', label: 'DOCX', group: 'Document' },
|
||||
{ value: 'doc', label: 'DOC', group: 'Document' },
|
||||
@ -65,6 +73,7 @@ export const FROM_FORMAT_OPTIONS = [
|
||||
{ value: 'bmp', label: 'BMP', group: 'Image' },
|
||||
{ value: 'tiff', label: 'TIFF', group: 'Image' },
|
||||
{ value: 'webp', label: 'WEBP', group: 'Image' },
|
||||
{ value: 'svg', label: 'SVG', group: 'Image' },
|
||||
{ value: 'html', label: 'HTML', group: 'Web' },
|
||||
{ value: 'htm', label: 'HTM', group: 'Web' },
|
||||
{ value: 'md', label: 'MD', group: 'Text' },
|
||||
@ -96,11 +105,13 @@ export const TO_FORMAT_OPTIONS = [
|
||||
|
||||
// Conversion matrix - what each source format can convert to
|
||||
export const CONVERSION_MATRIX: Record<string, string[]> = {
|
||||
'any': ['pdf'], // Mixed files always convert to PDF
|
||||
'image': ['pdf'], // Multiple images always convert to PDF
|
||||
'pdf': ['png', 'jpg', 'gif', 'tiff', 'bmp', 'webp', 'docx', 'odt', 'pptx', 'odp', 'csv', 'txt', 'rtf', 'md', 'html', 'xml', 'pdfa'],
|
||||
'docx': ['pdf'], 'doc': ['pdf'], 'odt': ['pdf'],
|
||||
'xlsx': ['pdf'], 'xls': ['pdf'], 'ods': ['pdf'],
|
||||
'pptx': ['pdf'], 'ppt': ['pdf'], 'odp': ['pdf'],
|
||||
'jpg': ['pdf'], 'jpeg': ['pdf'], 'png': ['pdf'], 'gif': ['pdf'], 'bmp': ['pdf'], 'tiff': ['pdf'], 'webp': ['pdf'],
|
||||
'jpg': ['pdf'], 'jpeg': ['pdf'], 'png': ['pdf'], 'gif': ['pdf'], 'bmp': ['pdf'], 'tiff': ['pdf'], 'webp': ['pdf'], 'svg': ['pdf'],
|
||||
'html': ['pdf'], 'htm': ['pdf'],
|
||||
'md': ['pdf'],
|
||||
'txt': ['pdf'], 'rtf': ['pdf'],
|
||||
@ -109,6 +120,8 @@ export const CONVERSION_MATRIX: Record<string, string[]> = {
|
||||
|
||||
// Map extensions to endpoint keys
|
||||
export const EXTENSION_TO_ENDPOINT: Record<string, Record<string, string>> = {
|
||||
'any': { 'pdf': 'file-to-pdf' }, // Mixed files use file-to-pdf endpoint
|
||||
'image': { 'pdf': 'img-to-pdf' }, // Multiple images use img-to-pdf endpoint
|
||||
'pdf': {
|
||||
'png': 'pdf-to-img', 'jpg': 'pdf-to-img', 'gif': 'pdf-to-img', 'tiff': 'pdf-to-img', 'bmp': 'pdf-to-img', 'webp': 'pdf-to-img',
|
||||
'docx': 'pdf-to-word', 'odt': 'pdf-to-word',
|
||||
@ -122,7 +135,7 @@ export const EXTENSION_TO_ENDPOINT: Record<string, Record<string, string>> = {
|
||||
'xlsx': { 'pdf': 'file-to-pdf' }, 'xls': { 'pdf': 'file-to-pdf' }, 'ods': { 'pdf': 'file-to-pdf' },
|
||||
'pptx': { 'pdf': 'file-to-pdf' }, 'ppt': { 'pdf': 'file-to-pdf' }, 'odp': { 'pdf': 'file-to-pdf' },
|
||||
'jpg': { 'pdf': 'img-to-pdf' }, 'jpeg': { 'pdf': 'img-to-pdf' }, 'png': { 'pdf': 'img-to-pdf' },
|
||||
'gif': { 'pdf': 'img-to-pdf' }, 'bmp': { 'pdf': 'img-to-pdf' }, 'tiff': { 'pdf': 'img-to-pdf' }, 'webp': { 'pdf': 'img-to-pdf' },
|
||||
'gif': { 'pdf': 'img-to-pdf' }, 'bmp': { 'pdf': 'img-to-pdf' }, 'tiff': { 'pdf': 'img-to-pdf' }, 'webp': { 'pdf': 'img-to-pdf' }, 'svg': { 'pdf': 'img-to-pdf' },
|
||||
'html': { 'pdf': 'html-to-pdf' }, 'htm': { 'pdf': 'html-to-pdf' },
|
||||
'md': { 'pdf': 'markdown-to-pdf' },
|
||||
'txt': { 'pdf': 'file-to-pdf' }, 'rtf': { 'pdf': 'file-to-pdf' },
|
||||
@ -131,3 +144,4 @@ export const EXTENSION_TO_ENDPOINT: Record<string, Record<string, string>> = {
|
||||
|
||||
export type ColorType = typeof COLOR_TYPES[keyof typeof COLOR_TYPES];
|
||||
export type OutputOption = typeof OUTPUT_OPTIONS[keyof typeof OUTPUT_OPTIONS];
|
||||
export type FitOption = typeof FIT_OPTIONS[keyof typeof FIT_OPTIONS];
|
@ -4,13 +4,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useFileContext } from '../../../contexts/FileContext';
|
||||
import { FileOperation } from '../../../types/fileContext';
|
||||
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
|
||||
import { makeApiUrl } from '../../../utils/api';
|
||||
import { ConvertParameters } from './useConvertParameters';
|
||||
import {
|
||||
CONVERSION_ENDPOINTS,
|
||||
ENDPOINT_NAMES,
|
||||
EXTENSION_TO_ENDPOINT
|
||||
} from '../../../constants/convertConstants';
|
||||
|
||||
import { getEndpointUrl, isImageFormat } from '../../../utils/convertUtils';
|
||||
|
||||
export interface ConvertOperationHook {
|
||||
@ -77,13 +72,13 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
||||
formData.append("outputFormat", toExtension);
|
||||
} else if (fromExtension === 'pdf' && ['txt', 'rtf'].includes(toExtension)) {
|
||||
formData.append("outputFormat", toExtension);
|
||||
} else if (isImageFormat(fromExtension) && toExtension === 'pdf') {
|
||||
formData.append("fitOption", "fillPage");
|
||||
} else if ((isImageFormat(fromExtension) || fromExtension === 'image') && toExtension === 'pdf') {
|
||||
formData.append("fitOption", imageOptions.fitOption);
|
||||
formData.append("colorType", imageOptions.colorType);
|
||||
formData.append("autoRotate", "true");
|
||||
formData.append("autoRotate", imageOptions.autoRotate.toString());
|
||||
} else if (fromExtension === 'pdf' && toExtension === 'csv') {
|
||||
// CSV extraction requires page numbers parameter
|
||||
formData.append("pageNumbers", parameters.pageNumbers || "all");
|
||||
// CSV extraction - always process all pages for simplified workflow
|
||||
formData.append("pageNumbers", "all");
|
||||
}
|
||||
|
||||
return formData;
|
||||
@ -107,7 +102,6 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
||||
parameters: {
|
||||
fromExtension: parameters.fromExtension,
|
||||
toExtension: parameters.toExtension,
|
||||
pageNumbers: parameters.pageNumbers,
|
||||
imageOptions: parameters.imageOptions,
|
||||
},
|
||||
fileSize: selectedFiles[0].size
|
||||
@ -154,6 +148,123 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this should be processed as separate files
|
||||
const shouldProcessSeparately = selectedFiles.length > 1 && (
|
||||
// Image to PDF with combineImages = false
|
||||
((isImageFormat(parameters.fromExtension) || parameters.fromExtension === 'image') &&
|
||||
parameters.toExtension === 'pdf' && !parameters.imageOptions.combineImages) ||
|
||||
// Mixed file types (smart detection)
|
||||
(parameters.isSmartDetection && parameters.smartDetectionType === 'mixed')
|
||||
);
|
||||
|
||||
if (shouldProcessSeparately) {
|
||||
// Process each file separately with appropriate endpoint
|
||||
await executeMultipleSeparateFiles(parameters, selectedFiles);
|
||||
} else {
|
||||
// Process all files together (default behavior)
|
||||
await executeSingleCombinedOperation(parameters, selectedFiles);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const executeMultipleSeparateFiles = async (
|
||||
parameters: ConvertParameters,
|
||||
selectedFiles: File[]
|
||||
) => {
|
||||
setStatus(t("loading"));
|
||||
setIsLoading(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
const results: File[] = [];
|
||||
const thumbnails: string[] = [];
|
||||
|
||||
try {
|
||||
// Process each file separately
|
||||
for (let i = 0; i < selectedFiles.length; i++) {
|
||||
const file = selectedFiles[i];
|
||||
setStatus(t("convert.processingFile", `Processing file ${i + 1} of ${selectedFiles.length}...`));
|
||||
|
||||
// Detect the specific file type for this file
|
||||
const fileExtension = file.name.split('.').pop()?.toLowerCase() || '';
|
||||
|
||||
// Determine the best endpoint for this specific file type
|
||||
let endpoint = getEndpointUrl(fileExtension, parameters.toExtension);
|
||||
let fileSpecificParams = { ...parameters, fromExtension: fileExtension };
|
||||
|
||||
// Fallback to file-to-pdf if specific endpoint doesn't exist
|
||||
if (!endpoint && parameters.toExtension === 'pdf') {
|
||||
endpoint = '/api/v1/convert/file/pdf';
|
||||
console.log(`Using file-to-pdf fallback for ${fileExtension} file: ${file.name}`);
|
||||
}
|
||||
|
||||
if (!endpoint) {
|
||||
console.error(`No endpoint available for ${fileExtension} to ${parameters.toExtension}`);
|
||||
continue; // Skip this file
|
||||
}
|
||||
|
||||
// Create individual operation for this file
|
||||
const { operation, operationId, fileId } = createOperation(fileSpecificParams, [file]);
|
||||
const formData = buildFormData(fileSpecificParams, [file]);
|
||||
|
||||
recordOperation(fileId, operation);
|
||||
|
||||
try {
|
||||
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
||||
const blob = new Blob([response.data]);
|
||||
|
||||
// Generate filename for this specific file
|
||||
const originalName = file.name.split('.')[0];
|
||||
const filename = `${originalName}_converted.${parameters.toExtension}`;
|
||||
const convertedFile = new File([blob], filename, { type: blob.type });
|
||||
|
||||
results.push(convertedFile);
|
||||
|
||||
// Generate thumbnail
|
||||
try {
|
||||
const thumbnail = await generateThumbnailForFile(convertedFile);
|
||||
thumbnails.push(thumbnail);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to generate thumbnail for ${filename}:`, error);
|
||||
thumbnails.push('');
|
||||
}
|
||||
|
||||
markOperationApplied(fileId, operationId);
|
||||
} catch (error: any) {
|
||||
console.error(`Error converting file ${file.name}:`, error);
|
||||
markOperationFailed(fileId, operationId);
|
||||
// Continue with other files even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
if (results.length > 0) {
|
||||
// Set results for multiple files
|
||||
setFiles(results);
|
||||
setThumbnails(thumbnails);
|
||||
|
||||
// Add all converted files to FileContext
|
||||
await addFiles(results);
|
||||
|
||||
// For multiple separate files, use the first file for download
|
||||
const firstFileBlob = new Blob([results[0]]);
|
||||
const firstFileUrl = window.URL.createObjectURL(firstFileBlob);
|
||||
|
||||
setDownloadUrl(firstFileUrl);
|
||||
setDownloadFilename(results[0].name);
|
||||
setStatus(t("convert.multipleFilesComplete", `Converted ${results.length} files successfully`));
|
||||
} else {
|
||||
setErrorMessage(t("convert.errorAllFilesFailed", "All files failed to convert"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in multiple operations:', error);
|
||||
setErrorMessage(t("convert.errorMultipleConversion", "An error occurred while converting multiple files"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const executeSingleCombinedOperation = async (
|
||||
parameters: ConvertParameters,
|
||||
selectedFiles: File[]
|
||||
) => {
|
||||
const { operation, operationId, fileId } = createOperation(parameters, selectedFiles);
|
||||
const formData = buildFormData(parameters, selectedFiles);
|
||||
|
||||
@ -176,7 +287,61 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// Generate filename based on conversion
|
||||
const originalName = selectedFiles[0].name.split('.')[0];
|
||||
const originalName = selectedFiles.length === 1
|
||||
? selectedFiles[0].name.split('.')[0]
|
||||
: 'combined_images';
|
||||
const filename = `${originalName}_converted.${parameters.toExtension}`;
|
||||
|
||||
setDownloadUrl(url);
|
||||
setDownloadFilename(filename);
|
||||
setStatus(t("downloadComplete"));
|
||||
|
||||
await processResults(blob, filename);
|
||||
markOperationApplied(fileId, operationId);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
let errorMsg = t("convert.errorConversion", "An error occurred while converting the file.");
|
||||
if (error.response?.data && typeof error.response.data === 'string') {
|
||||
errorMsg = error.response.data;
|
||||
} else if (error.message) {
|
||||
errorMsg = error.message;
|
||||
}
|
||||
setErrorMessage(errorMsg);
|
||||
markOperationFailed(fileId, operationId, errorMsg);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const executeSingleOperation = useCallback(async (
|
||||
parameters: ConvertParameters,
|
||||
selectedFiles: File[]
|
||||
) => {
|
||||
const { operation, operationId, fileId } = createOperation(parameters, selectedFiles);
|
||||
const formData = buildFormData(parameters, selectedFiles);
|
||||
|
||||
// Get endpoint using utility function
|
||||
const endpoint = getEndpointUrl(parameters.fromExtension, parameters.toExtension);
|
||||
if (!endpoint) {
|
||||
setErrorMessage(t("convert.errorNotSupported", { from: parameters.fromExtension, to: parameters.toExtension }));
|
||||
return;
|
||||
}
|
||||
|
||||
recordOperation(fileId, operation);
|
||||
|
||||
setStatus(t("loading"));
|
||||
setIsLoading(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
||||
const blob = new Blob([response.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// Generate filename based on conversion
|
||||
const originalName = selectedFiles.length === 1
|
||||
? selectedFiles[0].name.split('.')[0]
|
||||
: 'combined_images';
|
||||
const filename = `${originalName}_converted.${parameters.toExtension}`;
|
||||
|
||||
setDownloadUrl(url);
|
||||
|
@ -1,23 +1,29 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
COLOR_TYPES,
|
||||
OUTPUT_OPTIONS,
|
||||
FIT_OPTIONS,
|
||||
TO_FORMAT_OPTIONS,
|
||||
CONVERSION_MATRIX,
|
||||
type ColorType,
|
||||
type OutputOption
|
||||
type OutputOption,
|
||||
type FitOption
|
||||
} from '../../../constants/convertConstants';
|
||||
import { getEndpointName as getEndpointNameUtil, getEndpointUrl } from '../../../utils/convertUtils';
|
||||
import { getEndpointName as getEndpointNameUtil, getEndpointUrl, isImageFormat } from '../../../utils/convertUtils';
|
||||
|
||||
export interface ConvertParameters {
|
||||
fromExtension: string;
|
||||
toExtension: string;
|
||||
pageNumbers: string;
|
||||
imageOptions: {
|
||||
colorType: ColorType;
|
||||
dpi: number;
|
||||
singleOrMultiple: OutputOption;
|
||||
fitOption: FitOption;
|
||||
autoRotate: boolean;
|
||||
combineImages: boolean;
|
||||
};
|
||||
isSmartDetection: boolean;
|
||||
smartDetectionType: 'mixed' | 'images' | 'none';
|
||||
}
|
||||
|
||||
export interface ConvertParametersHook {
|
||||
@ -29,17 +35,22 @@ export interface ConvertParametersHook {
|
||||
getEndpoint: () => string;
|
||||
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
||||
detectFileExtension: (filename: string) => string;
|
||||
analyzeFileTypes: (files: Array<{name: string}>) => void;
|
||||
}
|
||||
|
||||
const initialParameters: ConvertParameters = {
|
||||
fromExtension: '',
|
||||
toExtension: '',
|
||||
pageNumbers: 'all',
|
||||
imageOptions: {
|
||||
colorType: COLOR_TYPES.COLOR,
|
||||
dpi: 300,
|
||||
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
||||
fitOption: FIT_OPTIONS.MAINTAIN_ASPECT,
|
||||
autoRotate: true,
|
||||
combineImages: true,
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none',
|
||||
};
|
||||
|
||||
export const useConvertParameters = (): ConvertParametersHook => {
|
||||
@ -73,12 +84,34 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
||||
};
|
||||
|
||||
const getEndpointName = () => {
|
||||
const { fromExtension, toExtension } = parameters;
|
||||
const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = parameters;
|
||||
|
||||
if (isSmartDetection) {
|
||||
if (smartDetectionType === 'mixed') {
|
||||
// Mixed file types -> PDF using file-to-pdf endpoint
|
||||
return 'file-to-pdf';
|
||||
} else if (smartDetectionType === 'images') {
|
||||
// All images -> PDF using img-to-pdf endpoint
|
||||
return 'img-to-pdf';
|
||||
}
|
||||
}
|
||||
|
||||
return getEndpointNameUtil(fromExtension, toExtension);
|
||||
};
|
||||
|
||||
const getEndpoint = () => {
|
||||
const { fromExtension, toExtension } = parameters;
|
||||
const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = parameters;
|
||||
|
||||
if (isSmartDetection) {
|
||||
if (smartDetectionType === 'mixed') {
|
||||
// Mixed file types -> PDF using file-to-pdf endpoint
|
||||
return '/api/v1/convert/file/pdf';
|
||||
} else if (smartDetectionType === 'images') {
|
||||
// All images -> PDF using img-to-pdf endpoint
|
||||
return '/api/v1/convert/img/pdf';
|
||||
}
|
||||
}
|
||||
|
||||
return getEndpointUrl(fromExtension, toExtension);
|
||||
};
|
||||
|
||||
@ -96,6 +129,66 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
||||
return extension || '';
|
||||
};
|
||||
|
||||
const analyzeFileTypes = (files: Array<{name: string}>) => {
|
||||
if (files.length <= 1) {
|
||||
// Single file or no files - use regular detection with auto-target selection
|
||||
const fromExt = files.length === 1 ? detectFileExtension(files[0].name) : '';
|
||||
const availableTargets = fromExt ? CONVERSION_MATRIX[fromExt] || [] : [];
|
||||
const autoTarget = availableTargets.length === 1 ? availableTargets[0] : '';
|
||||
|
||||
setParameters(prev => ({
|
||||
...prev,
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none',
|
||||
fromExtension: fromExt,
|
||||
toExtension: autoTarget
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Multiple files - analyze file types
|
||||
const extensions = files.map(file => detectFileExtension(file.name));
|
||||
const uniqueExtensions = [...new Set(extensions)];
|
||||
|
||||
if (uniqueExtensions.length === 1) {
|
||||
// All files are the same type - use regular detection with auto-target selection
|
||||
const fromExt = uniqueExtensions[0];
|
||||
const availableTargets = CONVERSION_MATRIX[fromExt] || [];
|
||||
const autoTarget = availableTargets.length === 1 ? availableTargets[0] : '';
|
||||
|
||||
setParameters(prev => ({
|
||||
...prev,
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none',
|
||||
fromExtension: fromExt,
|
||||
toExtension: autoTarget
|
||||
}));
|
||||
} else {
|
||||
// Mixed file types
|
||||
const allImages = uniqueExtensions.every(ext => isImageFormat(ext));
|
||||
|
||||
if (allImages) {
|
||||
// All files are images - use image-to-pdf conversion
|
||||
setParameters(prev => ({
|
||||
...prev,
|
||||
isSmartDetection: true,
|
||||
smartDetectionType: 'images',
|
||||
fromExtension: 'image',
|
||||
toExtension: 'pdf'
|
||||
}));
|
||||
} else {
|
||||
// Mixed non-image types - use file-to-pdf conversion
|
||||
setParameters(prev => ({
|
||||
...prev,
|
||||
isSmartDetection: true,
|
||||
smartDetectionType: 'mixed',
|
||||
fromExtension: 'any',
|
||||
toExtension: 'pdf'
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
parameters,
|
||||
updateParameter,
|
||||
@ -105,5 +198,6 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
||||
getEndpoint,
|
||||
getAvailableToExtensions,
|
||||
detectFileExtension,
|
||||
analyzeFileTypes,
|
||||
};
|
||||
};
|
@ -196,7 +196,7 @@ function HomePageContent() {
|
||||
onFilesSelect={(files) => {
|
||||
files.forEach(addToActiveFiles);
|
||||
}}
|
||||
accept={["application/pdf"]}
|
||||
accept={["*/*"]}
|
||||
loading={false}
|
||||
showRecentFiles={true}
|
||||
maxRecentFiles={8}
|
||||
@ -286,7 +286,7 @@ function HomePageContent() {
|
||||
onFilesSelect={(files) => {
|
||||
files.forEach(addToActiveFiles);
|
||||
}}
|
||||
accept={["application/pdf"]}
|
||||
accept={["*/*"]}
|
||||
loading={false}
|
||||
showRecentFiles={true}
|
||||
maxRecentFiles={8}
|
||||
|
@ -9,37 +9,67 @@
|
||||
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
import {
|
||||
ConversionEndpointDiscovery,
|
||||
conversionDiscovery,
|
||||
type ConversionEndpoint
|
||||
} from '../helpers/conversionEndpointDiscovery';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Test configuration
|
||||
const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8080';
|
||||
|
||||
// Test file paths (these would need to exist in your test fixtures)
|
||||
/**
|
||||
* Resolves test fixture paths dynamically based on current working directory.
|
||||
* Works from both top-level project directory and frontend subdirectory.
|
||||
*/
|
||||
function resolveTestFixturePath(filename: string): string {
|
||||
const cwd = process.cwd();
|
||||
|
||||
// Try frontend/src/tests/test-fixtures/ first (from top-level)
|
||||
const topLevelPath = path.join(cwd, 'frontend', 'src', 'tests', 'test-fixtures', filename);
|
||||
if (fs.existsSync(topLevelPath)) {
|
||||
return topLevelPath;
|
||||
}
|
||||
|
||||
// Try src/tests/test-fixtures/ (from frontend directory)
|
||||
const frontendPath = path.join(cwd, 'src', 'tests', 'test-fixtures', filename);
|
||||
if (fs.existsSync(frontendPath)) {
|
||||
return frontendPath;
|
||||
}
|
||||
|
||||
// Try relative path from current test file location
|
||||
const relativePath = path.join(__dirname, '..', 'test-fixtures', filename);
|
||||
if (fs.existsSync(relativePath)) {
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
// Fallback to the original path format (should work from top-level)
|
||||
return path.join('.', 'frontend', 'src', 'tests', 'test-fixtures', filename);
|
||||
}
|
||||
|
||||
// Test file paths (dynamically resolved based on current working directory)
|
||||
const TEST_FILES = {
|
||||
pdf: './src/tests/test-fixtures/sample.pdf',
|
||||
docx: './src/tests/test-fixtures/sample.docx',
|
||||
doc: './src/tests/test-fixtures/sample.doc',
|
||||
pptx: './src/tests/test-fixtures/sample.pptx',
|
||||
ppt: './src/tests/test-fixtures/sample.ppt',
|
||||
xlsx: './src/tests/test-fixtures/sample.xlsx',
|
||||
xls: './src/tests/test-fixtures/sample.xls',
|
||||
png: './src/tests/test-fixtures/sample.png',
|
||||
jpg: './src/tests/test-fixtures/sample.jpg',
|
||||
jpeg: './src/tests/test-fixtures/sample.jpeg',
|
||||
gif: './src/tests/test-fixtures/sample.gif',
|
||||
bmp: './src/tests/test-fixtures/sample.bmp',
|
||||
tiff: './src/tests/test-fixtures/sample.tiff',
|
||||
webp: './src/tests/test-fixtures/sample.webp',
|
||||
md: './src/tests/test-fixtures/sample.md',
|
||||
eml: './src/tests/test-fixtures/sample.eml',
|
||||
html: './src/tests/test-fixtures/sample.html',
|
||||
txt: './src/tests/test-fixtures/sample.txt',
|
||||
xml: './src/tests/test-fixtures/sample.xml',
|
||||
csv: './src/tests/test-fixtures/sample.csv'
|
||||
pdf: resolveTestFixturePath('sample.pdf'),
|
||||
docx: resolveTestFixturePath('sample.docx'),
|
||||
doc: resolveTestFixturePath('sample.doc'),
|
||||
pptx: resolveTestFixturePath('sample.pptx'),
|
||||
ppt: resolveTestFixturePath('sample.ppt'),
|
||||
xlsx: resolveTestFixturePath('sample.xlsx'),
|
||||
xls: resolveTestFixturePath('sample.xls'),
|
||||
png: resolveTestFixturePath('sample.png'),
|
||||
jpg: resolveTestFixturePath('sample.jpg'),
|
||||
jpeg: resolveTestFixturePath('sample.jpeg'),
|
||||
gif: resolveTestFixturePath('sample.gif'),
|
||||
bmp: resolveTestFixturePath('sample.bmp'),
|
||||
tiff: resolveTestFixturePath('sample.tiff'),
|
||||
webp: resolveTestFixturePath('sample.webp'),
|
||||
md: resolveTestFixturePath('sample.md'),
|
||||
eml: resolveTestFixturePath('sample.eml'),
|
||||
html: resolveTestFixturePath('sample.html'),
|
||||
txt: resolveTestFixturePath('sample.txt'),
|
||||
xml: resolveTestFixturePath('sample.xml'),
|
||||
csv: resolveTestFixturePath('sample.csv')
|
||||
};
|
||||
|
||||
// File format to test file mapping
|
||||
@ -303,7 +333,7 @@ test.describe('Convert Tool E2E Tests', () => {
|
||||
});
|
||||
|
||||
test('Image to PDF conversion', async ({ page }) => {
|
||||
const conversion = { endpoint: '/api/v1/convert/img/pdf', fromFormat: 'image', toFormat: 'pdf' };
|
||||
const conversion = { endpoint: '/api/v1/convert/img/pdf', fromFormat: 'png', toFormat: 'pdf' };
|
||||
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
||||
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
||||
|
||||
@ -400,8 +430,7 @@ test.describe('Convert Tool E2E Tests', () => {
|
||||
|
||||
test('should handle corrupted file gracefully', async ({ page }) => {
|
||||
// Create a corrupted file
|
||||
const fs = require('fs');
|
||||
const corruptedPath = './src/tests/test-fixtures/corrupted.pdf';
|
||||
const corruptedPath = resolveTestFixturePath('corrupted.pdf');
|
||||
fs.writeFileSync(corruptedPath, 'This is not a valid PDF file');
|
||||
|
||||
await page.setInputFiles('input[type="file"]', corruptedPath);
|
||||
|
@ -84,8 +84,13 @@ describe('Convert Tool Integration Tests', () => {
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple'
|
||||
}
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
@ -134,8 +139,13 @@ describe('Convert Tool Integration Tests', () => {
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple'
|
||||
}
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
@ -162,8 +172,13 @@ describe('Convert Tool Integration Tests', () => {
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple'
|
||||
}
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
@ -189,11 +204,17 @@ describe('Convert Tool Integration Tests', () => {
|
||||
const parameters: ConvertParameters = {
|
||||
fromExtension: 'pdf',
|
||||
toExtension: 'jpg',
|
||||
pageNumbers: 'all',
|
||||
imageOptions: {
|
||||
colorType: 'grayscale',
|
||||
dpi: 150,
|
||||
singleOrMultiple: 'single'
|
||||
}
|
||||
singleOrMultiple: 'single',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
@ -214,6 +235,57 @@ describe('Convert Tool Integration Tests', () => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
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({
|
||||
data: mockBlob,
|
||||
status: 200,
|
||||
statusText: 'OK'
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useConvertOperation(), {
|
||||
wrapper: TestWrapper
|
||||
});
|
||||
|
||||
const testFile = createPDFFile();
|
||||
const parameters: ConvertParameters = {
|
||||
fromExtension: 'pdf',
|
||||
toExtension: 'csv',
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
await result.current.executeOperation(parameters, [testFile]);
|
||||
});
|
||||
|
||||
// Verify correct endpoint is called
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/convert/pdf/csv',
|
||||
expect.any(FormData),
|
||||
{ responseType: 'blob' }
|
||||
);
|
||||
|
||||
// Verify FormData contains correct parameters for simplified CSV conversion
|
||||
const formDataCall = mockedAxios.post.mock.calls[0][1] as FormData;
|
||||
expect(formDataCall.get('pageNumbers')).toBe('all'); // Always "all" for simplified workflow
|
||||
expect(formDataCall.get('fileInput')).toBe(testFile);
|
||||
|
||||
// Verify hook state updates correctly
|
||||
expect(result.current.downloadUrl).toBeTruthy();
|
||||
expect(result.current.downloadFilename).toBe('test_converted.csv');
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.errorMessage).toBe(null);
|
||||
});
|
||||
|
||||
test('should handle complete unsupported conversion workflow', async () => {
|
||||
const { result } = renderHook(() => useConvertOperation(), {
|
||||
wrapper: TestWrapper
|
||||
@ -226,8 +298,13 @@ describe('Convert Tool Integration Tests', () => {
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple'
|
||||
}
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
@ -260,8 +337,13 @@ describe('Convert Tool Integration Tests', () => {
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple'
|
||||
}
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
@ -287,8 +369,13 @@ describe('Convert Tool Integration Tests', () => {
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple'
|
||||
}
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
@ -321,8 +408,13 @@ describe('Convert Tool Integration Tests', () => {
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple'
|
||||
}
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
@ -352,8 +444,13 @@ describe('Convert Tool Integration Tests', () => {
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple'
|
||||
}
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
@ -382,8 +479,13 @@ describe('Convert Tool Integration Tests', () => {
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple'
|
||||
}
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
@ -411,8 +513,13 @@ describe('Convert Tool Integration Tests', () => {
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple'
|
||||
}
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
|
@ -65,8 +65,13 @@ describe('Convert Tool Navigation Tests', () => {
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple'
|
||||
}
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
},
|
||||
updateParameter: mockOnParameterChange,
|
||||
resetParameters: vi.fn(),
|
||||
@ -99,7 +104,16 @@ describe('Convert Tool Navigation Tests', () => {
|
||||
parameters={{
|
||||
fromExtension: '',
|
||||
toExtension: '',
|
||||
imageOptions: { colorType: 'color', dpi: 300, singleOrMultiple: 'multiple' }
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
}}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||
@ -131,7 +145,16 @@ describe('Convert Tool Navigation Tests', () => {
|
||||
parameters={{
|
||||
fromExtension: '',
|
||||
toExtension: '',
|
||||
imageOptions: { colorType: 'color', dpi: 300, singleOrMultiple: 'multiple' }
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
}}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||
@ -169,7 +192,16 @@ describe('Convert Tool Navigation Tests', () => {
|
||||
parameters={{
|
||||
fromExtension: 'pdf',
|
||||
toExtension: '',
|
||||
imageOptions: { colorType: 'color', dpi: 300, singleOrMultiple: 'multiple' }
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
}}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||
@ -204,7 +236,16 @@ describe('Convert Tool Navigation Tests', () => {
|
||||
parameters={{
|
||||
fromExtension: 'pdf',
|
||||
toExtension: 'png',
|
||||
imageOptions: { colorType: 'color', dpi: 300, singleOrMultiple: 'multiple' }
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
}}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||
@ -230,7 +271,16 @@ describe('Convert Tool Navigation Tests', () => {
|
||||
parameters={{
|
||||
fromExtension: 'eml',
|
||||
toExtension: 'pdf',
|
||||
imageOptions: { colorType: 'color', dpi: 300, singleOrMultiple: 'multiple' }
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
}}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||
@ -260,7 +310,16 @@ describe('Convert Tool Navigation Tests', () => {
|
||||
parameters={{
|
||||
fromExtension: 'pdf',
|
||||
toExtension: 'png',
|
||||
imageOptions: { colorType: 'color', dpi: 300, singleOrMultiple: 'multiple' }
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
}}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||
@ -277,9 +336,9 @@ describe('Convert Tool Navigation Tests', () => {
|
||||
fireEvent.click(docxButton);
|
||||
});
|
||||
|
||||
// Should reset TO extension
|
||||
// Should change to docx default TO extension
|
||||
expect(mockOnParameterChange).toHaveBeenCalledWith('fromExtension', 'docx');
|
||||
expect(mockOnParameterChange).toHaveBeenCalledWith('toExtension', '');
|
||||
expect(mockOnParameterChange).toHaveBeenCalledWith('toExtension', 'pdf');
|
||||
});
|
||||
|
||||
test('should show placeholder when no FROM format is selected', () => {
|
||||
@ -289,7 +348,16 @@ describe('Convert Tool Navigation Tests', () => {
|
||||
parameters={{
|
||||
fromExtension: '',
|
||||
toExtension: '',
|
||||
imageOptions: { colorType: 'color', dpi: 300, singleOrMultiple: 'multiple' }
|
||||
imageOptions: {
|
||||
colorType: 'color',
|
||||
dpi: 300,
|
||||
singleOrMultiple: 'multiple',
|
||||
fitOption: 'maintainAspectRatio',
|
||||
autoRotate: true,
|
||||
combineImages: true
|
||||
},
|
||||
isSmartDetection: false,
|
||||
smartDetectionType: 'none'
|
||||
}}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import React, { useEffect, useMemo, useRef } from "react";
|
||||
import { Button, Stack, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
@ -22,6 +22,7 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { setCurrentMode } = useFileContext();
|
||||
const { selectedFiles } = useToolFileSelection();
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const convertParams = useConvertParameters();
|
||||
const convertOperation = useConvertOperation();
|
||||
@ -31,22 +32,50 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
convertParams.getEndpointName()
|
||||
);
|
||||
|
||||
// Auto-detect extension when files change
|
||||
// Auto-scroll to bottom when content grows
|
||||
const scrollToBottom = () => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTo({
|
||||
top: scrollContainerRef.current.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate state variables first
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = convertOperation.downloadUrl !== null;
|
||||
const filesCollapsed = hasFiles;
|
||||
const settingsCollapsed = hasResults;
|
||||
|
||||
// Auto-detect extension when files change - now with smart detection
|
||||
useEffect(() => {
|
||||
if (selectedFiles.length > 0 && !convertParams.parameters.fromExtension) {
|
||||
const firstFile = selectedFiles[0];
|
||||
const detectedExtension = convertParams.detectFileExtension(firstFile.name);
|
||||
if (detectedExtension) {
|
||||
convertParams.updateParameter('fromExtension', detectedExtension);
|
||||
if (selectedFiles.length > 0) {
|
||||
convertParams.analyzeFileTypes(selectedFiles);
|
||||
} else {
|
||||
convertParams.resetParameters();
|
||||
}
|
||||
}
|
||||
}, [selectedFiles, convertParams.parameters.fromExtension]);
|
||||
}, [selectedFiles]);
|
||||
|
||||
useEffect(() => {
|
||||
convertOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [convertParams.parameters, selectedFiles]);
|
||||
|
||||
// Auto-scroll when settings step becomes visible (files selected)
|
||||
useEffect(() => {
|
||||
if (hasFiles) {
|
||||
setTimeout(scrollToBottom, 100); // Small delay to ensure DOM update
|
||||
}
|
||||
}, [hasFiles]);
|
||||
|
||||
// Auto-scroll when results appear
|
||||
useEffect(() => {
|
||||
if (hasResults) {
|
||||
setTimeout(scrollToBottom, 100); // Small delay to ensure DOM update
|
||||
}
|
||||
}, [hasResults]);
|
||||
|
||||
const handleConvert = async () => {
|
||||
try {
|
||||
await convertOperation.executeOperation(
|
||||
@ -75,11 +104,6 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
setCurrentMode('convert');
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = convertOperation.downloadUrl !== null;
|
||||
const filesCollapsed = hasFiles;
|
||||
const settingsCollapsed = hasResults;
|
||||
|
||||
const previewResults = useMemo(() =>
|
||||
convertOperation.files?.map((file, index) => ({
|
||||
file,
|
||||
@ -89,8 +113,9 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full max-h-screen overflow-y-auto" ref={scrollContainerRef}>
|
||||
<ToolStepContainer>
|
||||
<Stack gap="sm" h="100%" p="sm" style={{ overflow: 'auto' }}>
|
||||
<Stack gap="sm" p="sm">
|
||||
{/* Files Step */}
|
||||
<ToolStep
|
||||
title="Files"
|
||||
@ -176,6 +201,7 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
</ToolStep>
|
||||
</Stack>
|
||||
</ToolStepContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -41,5 +41,5 @@ export const isConversionSupported = (fromExtension: string, toExtension: string
|
||||
* Checks if the given extension is an image format
|
||||
*/
|
||||
export const isImageFormat = (extension: string): boolean => {
|
||||
return ['png', 'jpg', 'jpeg', 'gif', 'tiff', 'bmp', 'webp'].includes(extension.toLowerCase());
|
||||
return ['png', 'jpg', 'jpeg', 'gif', 'tiff', 'bmp', 'webp', 'svg'].includes(extension.toLowerCase());
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user