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 {
|
} 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
|
<Dropzone
|
||||||
onDrop={handleFileUpload}
|
onDrop={handleFileUpload}
|
||||||
accept={["application/pdf", "application/zip", "application/x-zip-compressed"]}
|
accept={["*/*"]}
|
||||||
multiple={true}
|
multiple={true}
|
||||||
maxSize={2 * 1024 * 1024 * 1024}
|
maxSize={2 * 1024 * 1024 * 1024}
|
||||||
style={{ display: 'contents' }}
|
style={{ display: 'contents' }}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
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 { 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";
|
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
||||||
|
|
||||||
interface ConvertFromImageSettingsProps {
|
interface ConvertFromImageSettingsProps {
|
||||||
@ -35,6 +35,46 @@ const ConvertFromImageSettings = ({
|
|||||||
]}
|
]}
|
||||||
disabled={disabled}
|
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>
|
</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 GroupedFormatDropdown from "./GroupedFormatDropdown";
|
||||||
import ConvertToImageSettings from "./ConvertToImageSettings";
|
import ConvertToImageSettings from "./ConvertToImageSettings";
|
||||||
import ConvertFromImageSettings from "./ConvertFromImageSettings";
|
import ConvertFromImageSettings from "./ConvertFromImageSettings";
|
||||||
import ConvertFromPdfToCsvSettings from "./ConvertFromPdfToCsvSettings";
|
|
||||||
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
||||||
import {
|
import {
|
||||||
FROM_FORMAT_OPTIONS,
|
FROM_FORMAT_OPTIONS,
|
||||||
EXTENSION_TO_ENDPOINT,
|
EXTENSION_TO_ENDPOINT,
|
||||||
COLOR_TYPES,
|
COLOR_TYPES,
|
||||||
OUTPUT_OPTIONS
|
OUTPUT_OPTIONS,
|
||||||
|
FIT_OPTIONS
|
||||||
} from "../../../constants/convertConstants";
|
} from "../../../constants/convertConstants";
|
||||||
|
|
||||||
interface ConvertSettingsProps {
|
interface ConvertSettingsProps {
|
||||||
@ -84,14 +84,24 @@ const ConvertSettings = ({
|
|||||||
|
|
||||||
const handleFromExtensionChange = (value: string) => {
|
const handleFromExtensionChange = (value: string) => {
|
||||||
onParameterChange('fromExtension', value);
|
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
|
// Reset format-specific options
|
||||||
onParameterChange('imageOptions', {
|
onParameterChange('imageOptions', {
|
||||||
colorType: COLOR_TYPES.COLOR,
|
colorType: COLOR_TYPES.COLOR,
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
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) => {
|
const handleToExtensionChange = (value: string) => {
|
||||||
@ -101,13 +111,16 @@ const ConvertSettings = ({
|
|||||||
colorType: COLOR_TYPES.COLOR,
|
colorType: COLOR_TYPES.COLOR,
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
||||||
|
fitOption: FIT_OPTIONS.MAINTAIN_ASPECT,
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true,
|
||||||
});
|
});
|
||||||
onParameterChange('pageNumbers', 'all');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
|
|
||||||
{/* Format Selection */}
|
{/* Format Selection */}
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Text size="sm" fw={500}>
|
<Text size="sm" fw={500}>
|
||||||
@ -120,7 +133,7 @@ const ConvertSettings = ({
|
|||||||
placeholder={t("convert.sourceFormatPlaceholder", "Source format")}
|
placeholder={t("convert.sourceFormatPlaceholder", "Source format")}
|
||||||
options={enhancedFromOptions}
|
options={enhancedFromOptions}
|
||||||
onChange={handleFromExtensionChange}
|
onChange={handleFromExtensionChange}
|
||||||
disabled={disabled}
|
disabled={disabled || parameters.isSmartDetection}
|
||||||
minWidth="21.875rem"
|
minWidth="21.875rem"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
@ -150,6 +163,17 @@ const ConvertSettings = ({
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</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
|
<GroupedFormatDropdown
|
||||||
name="convert-to-dropdown"
|
name="convert-to-dropdown"
|
||||||
@ -178,7 +202,8 @@ const ConvertSettings = ({
|
|||||||
|
|
||||||
|
|
||||||
{/* Color options for image to PDF conversion */}
|
{/* 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 />
|
<Divider />
|
||||||
<ConvertFromImageSettings
|
<ConvertFromImageSettings
|
||||||
@ -187,7 +212,7 @@ const ConvertSettings = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
{/* EML specific options */}
|
{/* EML specific options */}
|
||||||
{parameters.fromExtension === 'eml' && parameters.toExtension === 'pdf' && (
|
{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>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -86,7 +86,9 @@ const GroupedFormatDropdown = ({
|
|||||||
: theme.white,
|
: theme.white,
|
||||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
width: '100%',
|
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">
|
<Group justify="space-between">
|
||||||
|
@ -10,6 +10,12 @@ export const OUTPUT_OPTIONS = {
|
|||||||
MULTIPLE: 'multiple'
|
MULTIPLE: 'multiple'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const FIT_OPTIONS = {
|
||||||
|
FIT_PAGE: 'fitDocumentToPage',
|
||||||
|
MAINTAIN_ASPECT: 'maintainAspectRatio',
|
||||||
|
FILL_PAGE: 'fillPage'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
|
||||||
export const CONVERSION_ENDPOINTS = {
|
export const CONVERSION_ENDPOINTS = {
|
||||||
'office-pdf': '/api/v1/convert/file/pdf',
|
'office-pdf': '/api/v1/convert/file/pdf',
|
||||||
@ -48,6 +54,8 @@ export const ENDPOINT_NAMES = {
|
|||||||
|
|
||||||
// Grouped file extensions for dropdowns
|
// Grouped file extensions for dropdowns
|
||||||
export const FROM_FORMAT_OPTIONS = [
|
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: 'pdf', label: 'PDF', group: 'Document' },
|
||||||
{ value: 'docx', label: 'DOCX', group: 'Document' },
|
{ value: 'docx', label: 'DOCX', group: 'Document' },
|
||||||
{ value: 'doc', label: 'DOC', group: 'Document' },
|
{ value: 'doc', label: 'DOC', group: 'Document' },
|
||||||
@ -65,6 +73,7 @@ export const FROM_FORMAT_OPTIONS = [
|
|||||||
{ value: 'bmp', label: 'BMP', group: 'Image' },
|
{ value: 'bmp', label: 'BMP', group: 'Image' },
|
||||||
{ value: 'tiff', label: 'TIFF', group: 'Image' },
|
{ value: 'tiff', label: 'TIFF', group: 'Image' },
|
||||||
{ value: 'webp', label: 'WEBP', group: 'Image' },
|
{ value: 'webp', label: 'WEBP', group: 'Image' },
|
||||||
|
{ value: 'svg', label: 'SVG', group: 'Image' },
|
||||||
{ value: 'html', label: 'HTML', group: 'Web' },
|
{ value: 'html', label: 'HTML', group: 'Web' },
|
||||||
{ value: 'htm', label: 'HTM', group: 'Web' },
|
{ value: 'htm', label: 'HTM', group: 'Web' },
|
||||||
{ value: 'md', label: 'MD', group: 'Text' },
|
{ value: 'md', label: 'MD', group: 'Text' },
|
||||||
@ -96,11 +105,13 @@ export const TO_FORMAT_OPTIONS = [
|
|||||||
|
|
||||||
// Conversion matrix - what each source format can convert to
|
// Conversion matrix - what each source format can convert to
|
||||||
export const CONVERSION_MATRIX: Record<string, string[]> = {
|
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'],
|
'pdf': ['png', 'jpg', 'gif', 'tiff', 'bmp', 'webp', 'docx', 'odt', 'pptx', 'odp', 'csv', 'txt', 'rtf', 'md', 'html', 'xml', 'pdfa'],
|
||||||
'docx': ['pdf'], 'doc': ['pdf'], 'odt': ['pdf'],
|
'docx': ['pdf'], 'doc': ['pdf'], 'odt': ['pdf'],
|
||||||
'xlsx': ['pdf'], 'xls': ['pdf'], 'ods': ['pdf'],
|
'xlsx': ['pdf'], 'xls': ['pdf'], 'ods': ['pdf'],
|
||||||
'pptx': ['pdf'], 'ppt': ['pdf'], 'odp': ['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'],
|
'html': ['pdf'], 'htm': ['pdf'],
|
||||||
'md': ['pdf'],
|
'md': ['pdf'],
|
||||||
'txt': ['pdf'], 'rtf': ['pdf'],
|
'txt': ['pdf'], 'rtf': ['pdf'],
|
||||||
@ -109,6 +120,8 @@ export const CONVERSION_MATRIX: Record<string, string[]> = {
|
|||||||
|
|
||||||
// Map extensions to endpoint keys
|
// Map extensions to endpoint keys
|
||||||
export const EXTENSION_TO_ENDPOINT: Record<string, Record<string, string>> = {
|
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': {
|
'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',
|
'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',
|
'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' },
|
'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' },
|
'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' },
|
'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' },
|
'html': { 'pdf': 'html-to-pdf' }, 'htm': { 'pdf': 'html-to-pdf' },
|
||||||
'md': { 'pdf': 'markdown-to-pdf' },
|
'md': { 'pdf': 'markdown-to-pdf' },
|
||||||
'txt': { 'pdf': 'file-to-pdf' }, 'rtf': { 'pdf': 'file-to-pdf' },
|
'txt': { 'pdf': 'file-to-pdf' }, 'rtf': { 'pdf': 'file-to-pdf' },
|
||||||
@ -130,4 +143,5 @@ export const EXTENSION_TO_ENDPOINT: Record<string, Record<string, string>> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ColorType = typeof COLOR_TYPES[keyof typeof COLOR_TYPES];
|
export type ColorType = typeof COLOR_TYPES[keyof typeof COLOR_TYPES];
|
||||||
export type OutputOption = typeof OUTPUT_OPTIONS[keyof typeof OUTPUT_OPTIONS];
|
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 { useFileContext } from '../../../contexts/FileContext';
|
||||||
import { FileOperation } from '../../../types/fileContext';
|
import { FileOperation } from '../../../types/fileContext';
|
||||||
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
|
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
|
||||||
import { makeApiUrl } from '../../../utils/api';
|
|
||||||
import { ConvertParameters } from './useConvertParameters';
|
import { ConvertParameters } from './useConvertParameters';
|
||||||
import {
|
|
||||||
CONVERSION_ENDPOINTS,
|
|
||||||
ENDPOINT_NAMES,
|
|
||||||
EXTENSION_TO_ENDPOINT
|
|
||||||
} from '../../../constants/convertConstants';
|
|
||||||
import { getEndpointUrl, isImageFormat } from '../../../utils/convertUtils';
|
import { getEndpointUrl, isImageFormat } from '../../../utils/convertUtils';
|
||||||
|
|
||||||
export interface ConvertOperationHook {
|
export interface ConvertOperationHook {
|
||||||
@ -77,13 +72,13 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
formData.append("outputFormat", toExtension);
|
formData.append("outputFormat", toExtension);
|
||||||
} else if (fromExtension === 'pdf' && ['txt', 'rtf'].includes(toExtension)) {
|
} else if (fromExtension === 'pdf' && ['txt', 'rtf'].includes(toExtension)) {
|
||||||
formData.append("outputFormat", toExtension);
|
formData.append("outputFormat", toExtension);
|
||||||
} else if (isImageFormat(fromExtension) && toExtension === 'pdf') {
|
} else if ((isImageFormat(fromExtension) || fromExtension === 'image') && toExtension === 'pdf') {
|
||||||
formData.append("fitOption", "fillPage");
|
formData.append("fitOption", imageOptions.fitOption);
|
||||||
formData.append("colorType", imageOptions.colorType);
|
formData.append("colorType", imageOptions.colorType);
|
||||||
formData.append("autoRotate", "true");
|
formData.append("autoRotate", imageOptions.autoRotate.toString());
|
||||||
} else if (fromExtension === 'pdf' && toExtension === 'csv') {
|
} else if (fromExtension === 'pdf' && toExtension === 'csv') {
|
||||||
// CSV extraction requires page numbers parameter
|
// CSV extraction - always process all pages for simplified workflow
|
||||||
formData.append("pageNumbers", parameters.pageNumbers || "all");
|
formData.append("pageNumbers", "all");
|
||||||
}
|
}
|
||||||
|
|
||||||
return formData;
|
return formData;
|
||||||
@ -107,7 +102,6 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
parameters: {
|
parameters: {
|
||||||
fromExtension: parameters.fromExtension,
|
fromExtension: parameters.fromExtension,
|
||||||
toExtension: parameters.toExtension,
|
toExtension: parameters.toExtension,
|
||||||
pageNumbers: parameters.pageNumbers,
|
|
||||||
imageOptions: parameters.imageOptions,
|
imageOptions: parameters.imageOptions,
|
||||||
},
|
},
|
||||||
fileSize: selectedFiles[0].size
|
fileSize: selectedFiles[0].size
|
||||||
@ -154,6 +148,123 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
return;
|
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 { operation, operationId, fileId } = createOperation(parameters, selectedFiles);
|
||||||
const formData = buildFormData(parameters, selectedFiles);
|
const formData = buildFormData(parameters, selectedFiles);
|
||||||
|
|
||||||
@ -176,7 +287,61 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
// Generate filename based on conversion
|
// 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}`;
|
const filename = `${originalName}_converted.${parameters.toExtension}`;
|
||||||
|
|
||||||
setDownloadUrl(url);
|
setDownloadUrl(url);
|
||||||
|
@ -1,23 +1,29 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
COLOR_TYPES,
|
COLOR_TYPES,
|
||||||
OUTPUT_OPTIONS,
|
OUTPUT_OPTIONS,
|
||||||
|
FIT_OPTIONS,
|
||||||
TO_FORMAT_OPTIONS,
|
TO_FORMAT_OPTIONS,
|
||||||
CONVERSION_MATRIX,
|
CONVERSION_MATRIX,
|
||||||
type ColorType,
|
type ColorType,
|
||||||
type OutputOption
|
type OutputOption,
|
||||||
|
type FitOption
|
||||||
} from '../../../constants/convertConstants';
|
} from '../../../constants/convertConstants';
|
||||||
import { getEndpointName as getEndpointNameUtil, getEndpointUrl } from '../../../utils/convertUtils';
|
import { getEndpointName as getEndpointNameUtil, getEndpointUrl, isImageFormat } from '../../../utils/convertUtils';
|
||||||
|
|
||||||
export interface ConvertParameters {
|
export interface ConvertParameters {
|
||||||
fromExtension: string;
|
fromExtension: string;
|
||||||
toExtension: string;
|
toExtension: string;
|
||||||
pageNumbers: string;
|
|
||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: ColorType;
|
colorType: ColorType;
|
||||||
dpi: number;
|
dpi: number;
|
||||||
singleOrMultiple: OutputOption;
|
singleOrMultiple: OutputOption;
|
||||||
|
fitOption: FitOption;
|
||||||
|
autoRotate: boolean;
|
||||||
|
combineImages: boolean;
|
||||||
};
|
};
|
||||||
|
isSmartDetection: boolean;
|
||||||
|
smartDetectionType: 'mixed' | 'images' | 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConvertParametersHook {
|
export interface ConvertParametersHook {
|
||||||
@ -29,17 +35,22 @@ export interface ConvertParametersHook {
|
|||||||
getEndpoint: () => string;
|
getEndpoint: () => string;
|
||||||
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
||||||
detectFileExtension: (filename: string) => string;
|
detectFileExtension: (filename: string) => string;
|
||||||
|
analyzeFileTypes: (files: Array<{name: string}>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialParameters: ConvertParameters = {
|
const initialParameters: ConvertParameters = {
|
||||||
fromExtension: '',
|
fromExtension: '',
|
||||||
toExtension: '',
|
toExtension: '',
|
||||||
pageNumbers: 'all',
|
|
||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: COLOR_TYPES.COLOR,
|
colorType: COLOR_TYPES.COLOR,
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
||||||
|
fitOption: FIT_OPTIONS.MAINTAIN_ASPECT,
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true,
|
||||||
},
|
},
|
||||||
|
isSmartDetection: false,
|
||||||
|
smartDetectionType: 'none',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useConvertParameters = (): ConvertParametersHook => {
|
export const useConvertParameters = (): ConvertParametersHook => {
|
||||||
@ -73,12 +84,34 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getEndpointName = () => {
|
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);
|
return getEndpointNameUtil(fromExtension, toExtension);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEndpoint = () => {
|
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);
|
return getEndpointUrl(fromExtension, toExtension);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,6 +129,66 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
|||||||
return extension || '';
|
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 {
|
return {
|
||||||
parameters,
|
parameters,
|
||||||
updateParameter,
|
updateParameter,
|
||||||
@ -105,5 +198,6 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
|||||||
getEndpoint,
|
getEndpoint,
|
||||||
getAvailableToExtensions,
|
getAvailableToExtensions,
|
||||||
detectFileExtension,
|
detectFileExtension,
|
||||||
|
analyzeFileTypes,
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -196,7 +196,7 @@ function HomePageContent() {
|
|||||||
onFilesSelect={(files) => {
|
onFilesSelect={(files) => {
|
||||||
files.forEach(addToActiveFiles);
|
files.forEach(addToActiveFiles);
|
||||||
}}
|
}}
|
||||||
accept={["application/pdf"]}
|
accept={["*/*"]}
|
||||||
loading={false}
|
loading={false}
|
||||||
showRecentFiles={true}
|
showRecentFiles={true}
|
||||||
maxRecentFiles={8}
|
maxRecentFiles={8}
|
||||||
@ -286,7 +286,7 @@ function HomePageContent() {
|
|||||||
onFilesSelect={(files) => {
|
onFilesSelect={(files) => {
|
||||||
files.forEach(addToActiveFiles);
|
files.forEach(addToActiveFiles);
|
||||||
}}
|
}}
|
||||||
accept={["application/pdf"]}
|
accept={["*/*"]}
|
||||||
loading={false}
|
loading={false}
|
||||||
showRecentFiles={true}
|
showRecentFiles={true}
|
||||||
maxRecentFiles={8}
|
maxRecentFiles={8}
|
||||||
|
@ -9,37 +9,67 @@
|
|||||||
|
|
||||||
import { test, expect, Page } from '@playwright/test';
|
import { test, expect, Page } from '@playwright/test';
|
||||||
import {
|
import {
|
||||||
ConversionEndpointDiscovery,
|
|
||||||
conversionDiscovery,
|
conversionDiscovery,
|
||||||
type ConversionEndpoint
|
type ConversionEndpoint
|
||||||
} from '../helpers/conversionEndpointDiscovery';
|
} from '../helpers/conversionEndpointDiscovery';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
// Test configuration
|
// Test configuration
|
||||||
const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
|
const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
|
||||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8080';
|
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 = {
|
const TEST_FILES = {
|
||||||
pdf: './src/tests/test-fixtures/sample.pdf',
|
pdf: resolveTestFixturePath('sample.pdf'),
|
||||||
docx: './src/tests/test-fixtures/sample.docx',
|
docx: resolveTestFixturePath('sample.docx'),
|
||||||
doc: './src/tests/test-fixtures/sample.doc',
|
doc: resolveTestFixturePath('sample.doc'),
|
||||||
pptx: './src/tests/test-fixtures/sample.pptx',
|
pptx: resolveTestFixturePath('sample.pptx'),
|
||||||
ppt: './src/tests/test-fixtures/sample.ppt',
|
ppt: resolveTestFixturePath('sample.ppt'),
|
||||||
xlsx: './src/tests/test-fixtures/sample.xlsx',
|
xlsx: resolveTestFixturePath('sample.xlsx'),
|
||||||
xls: './src/tests/test-fixtures/sample.xls',
|
xls: resolveTestFixturePath('sample.xls'),
|
||||||
png: './src/tests/test-fixtures/sample.png',
|
png: resolveTestFixturePath('sample.png'),
|
||||||
jpg: './src/tests/test-fixtures/sample.jpg',
|
jpg: resolveTestFixturePath('sample.jpg'),
|
||||||
jpeg: './src/tests/test-fixtures/sample.jpeg',
|
jpeg: resolveTestFixturePath('sample.jpeg'),
|
||||||
gif: './src/tests/test-fixtures/sample.gif',
|
gif: resolveTestFixturePath('sample.gif'),
|
||||||
bmp: './src/tests/test-fixtures/sample.bmp',
|
bmp: resolveTestFixturePath('sample.bmp'),
|
||||||
tiff: './src/tests/test-fixtures/sample.tiff',
|
tiff: resolveTestFixturePath('sample.tiff'),
|
||||||
webp: './src/tests/test-fixtures/sample.webp',
|
webp: resolveTestFixturePath('sample.webp'),
|
||||||
md: './src/tests/test-fixtures/sample.md',
|
md: resolveTestFixturePath('sample.md'),
|
||||||
eml: './src/tests/test-fixtures/sample.eml',
|
eml: resolveTestFixturePath('sample.eml'),
|
||||||
html: './src/tests/test-fixtures/sample.html',
|
html: resolveTestFixturePath('sample.html'),
|
||||||
txt: './src/tests/test-fixtures/sample.txt',
|
txt: resolveTestFixturePath('sample.txt'),
|
||||||
xml: './src/tests/test-fixtures/sample.xml',
|
xml: resolveTestFixturePath('sample.xml'),
|
||||||
csv: './src/tests/test-fixtures/sample.csv'
|
csv: resolveTestFixturePath('sample.csv')
|
||||||
};
|
};
|
||||||
|
|
||||||
// File format to test file mapping
|
// File format to test file mapping
|
||||||
@ -303,7 +333,7 @@ test.describe('Convert Tool E2E Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Image to PDF conversion', async ({ page }) => {
|
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);
|
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
||||||
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
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 }) => {
|
test('should handle corrupted file gracefully', async ({ page }) => {
|
||||||
// Create a corrupted file
|
// Create a corrupted file
|
||||||
const fs = require('fs');
|
const corruptedPath = resolveTestFixturePath('corrupted.pdf');
|
||||||
const corruptedPath = './src/tests/test-fixtures/corrupted.pdf';
|
|
||||||
fs.writeFileSync(corruptedPath, 'This is not a valid PDF file');
|
fs.writeFileSync(corruptedPath, 'This is not a valid PDF file');
|
||||||
|
|
||||||
await page.setInputFiles('input[type="file"]', corruptedPath);
|
await page.setInputFiles('input[type="file"]', corruptedPath);
|
||||||
|
@ -84,8 +84,13 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: 'color',
|
colorType: 'color',
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
singleOrMultiple: 'multiple'
|
singleOrMultiple: 'multiple',
|
||||||
}
|
fitOption: 'maintainAspectRatio',
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true
|
||||||
|
},
|
||||||
|
isSmartDetection: false,
|
||||||
|
smartDetectionType: 'none'
|
||||||
};
|
};
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@ -134,8 +139,13 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: 'color',
|
colorType: 'color',
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
singleOrMultiple: 'multiple'
|
singleOrMultiple: 'multiple',
|
||||||
}
|
fitOption: 'maintainAspectRatio',
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true
|
||||||
|
},
|
||||||
|
isSmartDetection: false,
|
||||||
|
smartDetectionType: 'none'
|
||||||
};
|
};
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@ -162,8 +172,13 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: 'color',
|
colorType: 'color',
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
singleOrMultiple: 'multiple'
|
singleOrMultiple: 'multiple',
|
||||||
}
|
fitOption: 'maintainAspectRatio',
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true
|
||||||
|
},
|
||||||
|
isSmartDetection: false,
|
||||||
|
smartDetectionType: 'none'
|
||||||
};
|
};
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@ -189,11 +204,17 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
const parameters: ConvertParameters = {
|
const parameters: ConvertParameters = {
|
||||||
fromExtension: 'pdf',
|
fromExtension: 'pdf',
|
||||||
toExtension: 'jpg',
|
toExtension: 'jpg',
|
||||||
|
pageNumbers: 'all',
|
||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: 'grayscale',
|
colorType: 'grayscale',
|
||||||
dpi: 150,
|
dpi: 150,
|
||||||
singleOrMultiple: 'single'
|
singleOrMultiple: 'single',
|
||||||
}
|
fitOption: 'maintainAspectRatio',
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true
|
||||||
|
},
|
||||||
|
isSmartDetection: false,
|
||||||
|
smartDetectionType: 'none'
|
||||||
};
|
};
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@ -214,6 +235,57 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
expect(result.current.isLoading).toBe(false);
|
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 () => {
|
test('should handle complete unsupported conversion workflow', async () => {
|
||||||
const { result } = renderHook(() => useConvertOperation(), {
|
const { result } = renderHook(() => useConvertOperation(), {
|
||||||
wrapper: TestWrapper
|
wrapper: TestWrapper
|
||||||
@ -226,8 +298,13 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: 'color',
|
colorType: 'color',
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
singleOrMultiple: 'multiple'
|
singleOrMultiple: 'multiple',
|
||||||
}
|
fitOption: 'maintainAspectRatio',
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true
|
||||||
|
},
|
||||||
|
isSmartDetection: false,
|
||||||
|
smartDetectionType: 'none'
|
||||||
};
|
};
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@ -260,8 +337,13 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: 'color',
|
colorType: 'color',
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
singleOrMultiple: 'multiple'
|
singleOrMultiple: 'multiple',
|
||||||
}
|
fitOption: 'maintainAspectRatio',
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true
|
||||||
|
},
|
||||||
|
isSmartDetection: false,
|
||||||
|
smartDetectionType: 'none'
|
||||||
};
|
};
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@ -287,8 +369,13 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: 'color',
|
colorType: 'color',
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
singleOrMultiple: 'multiple'
|
singleOrMultiple: 'multiple',
|
||||||
}
|
fitOption: 'maintainAspectRatio',
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true
|
||||||
|
},
|
||||||
|
isSmartDetection: false,
|
||||||
|
smartDetectionType: 'none'
|
||||||
};
|
};
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@ -321,8 +408,13 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: 'color',
|
colorType: 'color',
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
singleOrMultiple: 'multiple'
|
singleOrMultiple: 'multiple',
|
||||||
}
|
fitOption: 'maintainAspectRatio',
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true
|
||||||
|
},
|
||||||
|
isSmartDetection: false,
|
||||||
|
smartDetectionType: 'none'
|
||||||
};
|
};
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@ -352,8 +444,13 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: 'color',
|
colorType: 'color',
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
singleOrMultiple: 'multiple'
|
singleOrMultiple: 'multiple',
|
||||||
}
|
fitOption: 'maintainAspectRatio',
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true
|
||||||
|
},
|
||||||
|
isSmartDetection: false,
|
||||||
|
smartDetectionType: 'none'
|
||||||
};
|
};
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@ -382,8 +479,13 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: 'color',
|
colorType: 'color',
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
singleOrMultiple: 'multiple'
|
singleOrMultiple: 'multiple',
|
||||||
}
|
fitOption: 'maintainAspectRatio',
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true
|
||||||
|
},
|
||||||
|
isSmartDetection: false,
|
||||||
|
smartDetectionType: 'none'
|
||||||
};
|
};
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@ -411,8 +513,13 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: 'color',
|
colorType: 'color',
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
singleOrMultiple: 'multiple'
|
singleOrMultiple: 'multiple',
|
||||||
}
|
fitOption: 'maintainAspectRatio',
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true
|
||||||
|
},
|
||||||
|
isSmartDetection: false,
|
||||||
|
smartDetectionType: 'none'
|
||||||
};
|
};
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
@ -65,8 +65,13 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: 'color',
|
colorType: 'color',
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
singleOrMultiple: 'multiple'
|
singleOrMultiple: 'multiple',
|
||||||
}
|
fitOption: 'maintainAspectRatio',
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true
|
||||||
|
},
|
||||||
|
isSmartDetection: false,
|
||||||
|
smartDetectionType: 'none'
|
||||||
},
|
},
|
||||||
updateParameter: mockOnParameterChange,
|
updateParameter: mockOnParameterChange,
|
||||||
resetParameters: vi.fn(),
|
resetParameters: vi.fn(),
|
||||||
@ -99,7 +104,16 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
parameters={{
|
parameters={{
|
||||||
fromExtension: '',
|
fromExtension: '',
|
||||||
toExtension: '',
|
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}
|
onParameterChange={mockOnParameterChange}
|
||||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
@ -131,7 +145,16 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
parameters={{
|
parameters={{
|
||||||
fromExtension: '',
|
fromExtension: '',
|
||||||
toExtension: '',
|
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}
|
onParameterChange={mockOnParameterChange}
|
||||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
@ -169,7 +192,16 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
parameters={{
|
parameters={{
|
||||||
fromExtension: 'pdf',
|
fromExtension: 'pdf',
|
||||||
toExtension: '',
|
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}
|
onParameterChange={mockOnParameterChange}
|
||||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
@ -204,7 +236,16 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
parameters={{
|
parameters={{
|
||||||
fromExtension: 'pdf',
|
fromExtension: 'pdf',
|
||||||
toExtension: 'png',
|
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}
|
onParameterChange={mockOnParameterChange}
|
||||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
@ -230,7 +271,16 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
parameters={{
|
parameters={{
|
||||||
fromExtension: 'eml',
|
fromExtension: 'eml',
|
||||||
toExtension: 'pdf',
|
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}
|
onParameterChange={mockOnParameterChange}
|
||||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
@ -260,7 +310,16 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
parameters={{
|
parameters={{
|
||||||
fromExtension: 'pdf',
|
fromExtension: 'pdf',
|
||||||
toExtension: 'png',
|
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}
|
onParameterChange={mockOnParameterChange}
|
||||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
@ -277,9 +336,9 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
fireEvent.click(docxButton);
|
fireEvent.click(docxButton);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should reset TO extension
|
// Should change to docx default TO extension
|
||||||
expect(mockOnParameterChange).toHaveBeenCalledWith('fromExtension', 'docx');
|
expect(mockOnParameterChange).toHaveBeenCalledWith('fromExtension', 'docx');
|
||||||
expect(mockOnParameterChange).toHaveBeenCalledWith('toExtension', '');
|
expect(mockOnParameterChange).toHaveBeenCalledWith('toExtension', 'pdf');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show placeholder when no FROM format is selected', () => {
|
test('should show placeholder when no FROM format is selected', () => {
|
||||||
@ -289,7 +348,16 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
parameters={{
|
parameters={{
|
||||||
fromExtension: '',
|
fromExtension: '',
|
||||||
toExtension: '',
|
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}
|
onParameterChange={mockOnParameterChange}
|
||||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
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 { Button, Stack, Text } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import DownloadIcon from "@mui/icons-material/Download";
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
@ -22,6 +22,7 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setCurrentMode } = useFileContext();
|
const { setCurrentMode } = useFileContext();
|
||||||
const { selectedFiles } = useToolFileSelection();
|
const { selectedFiles } = useToolFileSelection();
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const convertParams = useConvertParameters();
|
const convertParams = useConvertParameters();
|
||||||
const convertOperation = useConvertOperation();
|
const convertOperation = useConvertOperation();
|
||||||
@ -31,22 +32,50 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
convertParams.getEndpointName()
|
convertParams.getEndpointName()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Auto-detect extension when files change
|
// Auto-scroll to bottom when content grows
|
||||||
useEffect(() => {
|
const scrollToBottom = () => {
|
||||||
if (selectedFiles.length > 0 && !convertParams.parameters.fromExtension) {
|
if (scrollContainerRef.current) {
|
||||||
const firstFile = selectedFiles[0];
|
scrollContainerRef.current.scrollTo({
|
||||||
const detectedExtension = convertParams.detectFileExtension(firstFile.name);
|
top: scrollContainerRef.current.scrollHeight,
|
||||||
if (detectedExtension) {
|
behavior: 'smooth'
|
||||||
convertParams.updateParameter('fromExtension', detectedExtension);
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [selectedFiles, convertParams.parameters.fromExtension]);
|
};
|
||||||
|
|
||||||
|
// 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.analyzeFileTypes(selectedFiles);
|
||||||
|
} else {
|
||||||
|
convertParams.resetParameters();
|
||||||
|
}
|
||||||
|
}, [selectedFiles]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
convertOperation.resetResults();
|
convertOperation.resetResults();
|
||||||
onPreviewFile?.(null);
|
onPreviewFile?.(null);
|
||||||
}, [convertParams.parameters, selectedFiles]);
|
}, [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 () => {
|
const handleConvert = async () => {
|
||||||
try {
|
try {
|
||||||
await convertOperation.executeOperation(
|
await convertOperation.executeOperation(
|
||||||
@ -75,11 +104,6 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
setCurrentMode('convert');
|
setCurrentMode('convert');
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasFiles = selectedFiles.length > 0;
|
|
||||||
const hasResults = convertOperation.downloadUrl !== null;
|
|
||||||
const filesCollapsed = hasFiles;
|
|
||||||
const settingsCollapsed = hasResults;
|
|
||||||
|
|
||||||
const previewResults = useMemo(() =>
|
const previewResults = useMemo(() =>
|
||||||
convertOperation.files?.map((file, index) => ({
|
convertOperation.files?.map((file, index) => ({
|
||||||
file,
|
file,
|
||||||
@ -89,8 +113,9 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolStepContainer>
|
<div className="h-full max-h-screen overflow-y-auto" ref={scrollContainerRef}>
|
||||||
<Stack gap="sm" h="100%" p="sm" style={{ overflow: 'auto' }}>
|
<ToolStepContainer>
|
||||||
|
<Stack gap="sm" p="sm">
|
||||||
{/* Files Step */}
|
{/* Files Step */}
|
||||||
<ToolStep
|
<ToolStep
|
||||||
title="Files"
|
title="Files"
|
||||||
@ -174,8 +199,9 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</ToolStep>
|
</ToolStep>
|
||||||
</Stack>
|
</Stack>
|
||||||
</ToolStepContainer>
|
</ToolStepContainer>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -41,5 +41,5 @@ export const isConversionSupported = (fromExtension: string, toExtension: string
|
|||||||
* Checks if the given extension is an image format
|
* Checks if the given extension is an image format
|
||||||
*/
|
*/
|
||||||
export const isImageFormat = (extension: string): boolean => {
|
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