mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Merge remote-tracking branch 'origin/V2' into V2-Results-Flow
This commit is contained in:
commit
184a1dd9ed
@ -38,7 +38,8 @@
|
|||||||
"save": "Save",
|
"save": "Save",
|
||||||
"saveToBrowser": "Save to Browser",
|
"saveToBrowser": "Save to Browser",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"filesSelected": "files selected",
|
"fileSelected": "Selected: {{filename}}",
|
||||||
|
"filesSelected": "{{count}} files selected",
|
||||||
"noFavourites": "No favourites added",
|
"noFavourites": "No favourites added",
|
||||||
"downloadComplete": "Download Complete",
|
"downloadComplete": "Download Complete",
|
||||||
"bored": "Bored Waiting?",
|
"bored": "Bored Waiting?",
|
||||||
@ -391,6 +392,10 @@
|
|||||||
"title": "Compress",
|
"title": "Compress",
|
||||||
"desc": "Compress PDFs to reduce their file size."
|
"desc": "Compress PDFs to reduce their file size."
|
||||||
},
|
},
|
||||||
|
"sanitize": {
|
||||||
|
"title": "Sanitise",
|
||||||
|
"desc": "Remove potentially harmful elements from PDF files."
|
||||||
|
},
|
||||||
"unlockPDFForms": {
|
"unlockPDFForms": {
|
||||||
"title": "Unlock PDF Forms",
|
"title": "Unlock PDF Forms",
|
||||||
"desc": "Remove read-only property of form fields in a PDF document."
|
"desc": "Remove read-only property of form fields in a PDF document."
|
||||||
@ -504,7 +509,7 @@
|
|||||||
"desc": "Auto Split Scanned PDF with physical scanned page splitter QR Code"
|
"desc": "Auto Split Scanned PDF with physical scanned page splitter QR Code"
|
||||||
},
|
},
|
||||||
"sanitizePdf": {
|
"sanitizePdf": {
|
||||||
"title": "Sanitize",
|
"title": "Sanitise",
|
||||||
"desc": "Remove scripts and other elements from PDF files"
|
"desc": "Remove scripts and other elements from PDF files"
|
||||||
},
|
},
|
||||||
"URLToPDF": {
|
"URLToPDF": {
|
||||||
@ -1425,8 +1430,8 @@
|
|||||||
"placeholder": "(e.g. 1,2,8 or 4,7,12-16 or 2n-1)"
|
"placeholder": "(e.g. 1,2,8 or 4,7,12-16 or 2n-1)"
|
||||||
},
|
},
|
||||||
"sanitizePDF": {
|
"sanitizePDF": {
|
||||||
"title": "Sanitize PDF",
|
"title": "Sanitise PDF",
|
||||||
"header": "Sanitize a PDF file",
|
"header": "Sanitise a PDF file",
|
||||||
"selectText": {
|
"selectText": {
|
||||||
"1": "Remove JavaScript actions",
|
"1": "Remove JavaScript actions",
|
||||||
"2": "Remove embedded files",
|
"2": "Remove embedded files",
|
||||||
@ -1761,5 +1766,37 @@
|
|||||||
"fileTooLarge": "File too large. Maximum size per file is",
|
"fileTooLarge": "File too large. Maximum size per file is",
|
||||||
"storageQuotaExceeded": "Storage quota exceeded. Please remove some files before uploading more.",
|
"storageQuotaExceeded": "Storage quota exceeded. Please remove some files before uploading more.",
|
||||||
"approximateSize": "Approximate size"
|
"approximateSize": "Approximate size"
|
||||||
|
},
|
||||||
|
"sanitize": {
|
||||||
|
"submit": "Sanitise PDF",
|
||||||
|
"completed": "Sanitisation completed successfully",
|
||||||
|
"error.generic": "Sanitisation failed",
|
||||||
|
"error.failed": "An error occurred while sanitising the PDF.",
|
||||||
|
"filenamePrefix": "sanitised",
|
||||||
|
"sanitizationResults": "Sanitisation Results",
|
||||||
|
"steps": {
|
||||||
|
"files": "Files",
|
||||||
|
"settings": "Settings",
|
||||||
|
"results": "Results"
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select a PDF file in the main view to get started"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"title": "Sanitisation Options",
|
||||||
|
"note": "Select the elements you want to remove from the PDF. At least one option must be selected.",
|
||||||
|
"removeJavaScript": "Remove JavaScript",
|
||||||
|
"removeJavaScript.desc": "Remove JavaScript actions and scripts from the PDF",
|
||||||
|
"removeEmbeddedFiles": "Remove Embedded Files",
|
||||||
|
"removeEmbeddedFiles.desc": "Remove any files embedded within the PDF",
|
||||||
|
"removeXMPMetadata": "Remove XMP Metadata",
|
||||||
|
"removeXMPMetadata.desc": "Remove XMP metadata from the PDF",
|
||||||
|
"removeMetadata": "Remove Document Metadata",
|
||||||
|
"removeMetadata.desc": "Remove document information metadata (title, author, etc.)",
|
||||||
|
"removeLinks": "Remove Links",
|
||||||
|
"removeLinks.desc": "Remove external links and launch actions from the PDF",
|
||||||
|
"removeFonts": "Remove Fonts",
|
||||||
|
"removeFonts.desc": "Remove embedded fonts from the PDF"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -38,7 +38,8 @@
|
|||||||
"save": "Save",
|
"save": "Save",
|
||||||
"saveToBrowser": "Save to Browser",
|
"saveToBrowser": "Save to Browser",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"filesSelected": "files selected",
|
"fileSelected": "Selected: {{filename}}",
|
||||||
|
"filesSelected": "{{count}} files selected",
|
||||||
"noFavourites": "No favorites added",
|
"noFavourites": "No favorites added",
|
||||||
"downloadComplete": "Download Complete",
|
"downloadComplete": "Download Complete",
|
||||||
"bored": "Bored Waiting?",
|
"bored": "Bored Waiting?",
|
||||||
@ -387,6 +388,10 @@
|
|||||||
"title": "Compress",
|
"title": "Compress",
|
||||||
"desc": "Compress PDFs to reduce their file size."
|
"desc": "Compress PDFs to reduce their file size."
|
||||||
},
|
},
|
||||||
|
"sanitize": {
|
||||||
|
"title": "Sanitize",
|
||||||
|
"desc": "Remove potentially harmful elements from PDF files."
|
||||||
|
},
|
||||||
"unlockPDFForms": {
|
"unlockPDFForms": {
|
||||||
"title": "Unlock PDF Forms",
|
"title": "Unlock PDF Forms",
|
||||||
"desc": "Remove read-only property of form fields in a PDF document."
|
"desc": "Remove read-only property of form fields in a PDF document."
|
||||||
@ -1612,6 +1617,38 @@
|
|||||||
"pdfaOptions": "PDF/A Options",
|
"pdfaOptions": "PDF/A Options",
|
||||||
"outputFormat": "Output Format",
|
"outputFormat": "Output Format",
|
||||||
"pdfaNote": "PDF/A-1b is more compatible, PDF/A-2b supports more features.",
|
"pdfaNote": "PDF/A-1b is more compatible, PDF/A-2b supports more features.",
|
||||||
"pdfaDigitalSignatureWarning": "The PDF contains a digital signature. This will be removed in the next step."
|
"pdfaDigitalSignatureWarning": "The PDF contains a digital signature. This will be removed in the next step.",
|
||||||
|
"sanitize": {
|
||||||
|
"submit": "Sanitize PDF",
|
||||||
|
"completed": "Sanitization completed successfully",
|
||||||
|
"error.generic": "Sanitization failed",
|
||||||
|
"error.failed": "An error occurred while sanitizing the PDF.",
|
||||||
|
"filenamePrefix": "sanitized",
|
||||||
|
"sanitizationResults": "Sanitization Results",
|
||||||
|
"steps": {
|
||||||
|
"files": "Files",
|
||||||
|
"settings": "Settings",
|
||||||
|
"results": "Results"
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select a PDF file in the main view to get started"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"title": "Sanitization Options",
|
||||||
|
"note": "Select the elements you want to remove from the PDF. At least one option must be selected.",
|
||||||
|
"removeJavaScript": "Remove JavaScript",
|
||||||
|
"removeJavaScript.desc": "Remove JavaScript actions and scripts from the PDF",
|
||||||
|
"removeEmbeddedFiles": "Remove Embedded Files",
|
||||||
|
"removeEmbeddedFiles.desc": "Remove any files embedded within the PDF",
|
||||||
|
"removeXMPMetadata": "Remove XMP Metadata",
|
||||||
|
"removeXMPMetadata.desc": "Remove XMP metadata from the PDF",
|
||||||
|
"removeMetadata": "Remove Document Metadata",
|
||||||
|
"removeMetadata.desc": "Remove document information metadata (title, author, etc.)",
|
||||||
|
"removeLinks": "Remove Links",
|
||||||
|
"removeLinks.desc": "Remove external links and launch actions from the PDF",
|
||||||
|
"removeFonts": "Remove Fonts",
|
||||||
|
"removeFonts.desc": "Remove embedded fonts from the PDF"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
194
frontend/src/components/tools/sanitize/SanitizeSettings.test.tsx
Normal file
194
frontend/src/components/tools/sanitize/SanitizeSettings.test.tsx
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { MantineProvider } from '@mantine/core';
|
||||||
|
import SanitizeSettings from './SanitizeSettings';
|
||||||
|
import { SanitizeParameters } from '../../../hooks/tools/sanitize/useSanitizeParameters';
|
||||||
|
|
||||||
|
// Mock useTranslation with predictable return values
|
||||||
|
const mockT = vi.fn((key: string) => `mock-${key}`);
|
||||||
|
vi.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({ t: mockT })
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Wrapper component to provide Mantine context
|
||||||
|
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<MantineProvider>{children}</MantineProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('SanitizeSettings', () => {
|
||||||
|
const defaultParameters: SanitizeParameters = {
|
||||||
|
removeJavaScript: true,
|
||||||
|
removeEmbeddedFiles: true,
|
||||||
|
removeXMPMetadata: false,
|
||||||
|
removeMetadata: false,
|
||||||
|
removeLinks: false,
|
||||||
|
removeFonts: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockOnParameterChange = vi.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render all sanitization option checkboxes', () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<SanitizeSettings
|
||||||
|
parameters={defaultParameters}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should render one checkbox for each parameter
|
||||||
|
const expectedCheckboxCount = Object.keys(defaultParameters).length;
|
||||||
|
const checkboxes = screen.getAllByRole('checkbox');
|
||||||
|
expect(checkboxes).toHaveLength(expectedCheckboxCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show correct initial checkbox states based on parameters', () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<SanitizeSettings
|
||||||
|
parameters={defaultParameters}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkboxes = screen.getAllByRole('checkbox');
|
||||||
|
const parameterValues = Object.values(defaultParameters);
|
||||||
|
|
||||||
|
parameterValues.forEach((value, index) => {
|
||||||
|
if (value) {
|
||||||
|
expect(checkboxes[index]).toBeChecked();
|
||||||
|
} else {
|
||||||
|
expect(checkboxes[index]).not.toBeChecked();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call onParameterChange with correct parameters when checkboxes are clicked', () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<SanitizeSettings
|
||||||
|
parameters={defaultParameters}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkboxes = screen.getAllByRole('checkbox');
|
||||||
|
|
||||||
|
// Click the first checkbox (removeJavaScript - should toggle from true to false)
|
||||||
|
fireEvent.click(checkboxes[0]);
|
||||||
|
expect(mockOnParameterChange).toHaveBeenCalledWith('removeJavaScript', false);
|
||||||
|
|
||||||
|
// Click the third checkbox (removeXMPMetadata - should toggle from false to true)
|
||||||
|
fireEvent.click(checkboxes[2]);
|
||||||
|
expect(mockOnParameterChange).toHaveBeenCalledWith('removeXMPMetadata', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should disable all checkboxes when disabled prop is true', () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<SanitizeSettings
|
||||||
|
parameters={defaultParameters}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkboxes = screen.getAllByRole('checkbox');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
expect(checkbox).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should enable all checkboxes when disabled prop is false or undefined', () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<SanitizeSettings
|
||||||
|
parameters={defaultParameters}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkboxes = screen.getAllByRole('checkbox');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
expect(checkbox).not.toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle different parameter combinations', () => {
|
||||||
|
const allEnabledParameters: SanitizeParameters = {
|
||||||
|
removeJavaScript: true,
|
||||||
|
removeEmbeddedFiles: true,
|
||||||
|
removeXMPMetadata: true,
|
||||||
|
removeMetadata: true,
|
||||||
|
removeLinks: true,
|
||||||
|
removeFonts: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<SanitizeSettings
|
||||||
|
parameters={allEnabledParameters}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkboxes = screen.getAllByRole('checkbox');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
expect(checkbox).toBeChecked();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call translation function with correct keys', () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<SanitizeSettings
|
||||||
|
parameters={defaultParameters}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that translation keys are being called (just check that it was called, not specific order)
|
||||||
|
expect(mockT).toHaveBeenCalledWith('sanitize.options.title', expect.any(String));
|
||||||
|
expect(mockT).toHaveBeenCalledWith('sanitize.options.removeJavaScript', expect.any(String));
|
||||||
|
expect(mockT).toHaveBeenCalledWith('sanitize.options.removeEmbeddedFiles', expect.any(String));
|
||||||
|
expect(mockT).toHaveBeenCalledWith('sanitize.options.note', expect.any(String));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not call onParameterChange when disabled', () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<SanitizeSettings
|
||||||
|
parameters={defaultParameters}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkboxes = screen.getAllByRole('checkbox');
|
||||||
|
|
||||||
|
// Verify checkboxes are disabled
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
expect(checkbox).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try to click a disabled checkbox - this might still fire the event in tests
|
||||||
|
// but we can verify the checkbox state doesn't actually change
|
||||||
|
const firstCheckbox = checkboxes[0] as HTMLInputElement;
|
||||||
|
const initialChecked = firstCheckbox.checked;
|
||||||
|
fireEvent.click(firstCheckbox);
|
||||||
|
expect(firstCheckbox.checked).toBe(initialChecked);
|
||||||
|
});
|
||||||
|
});
|
51
frontend/src/components/tools/sanitize/SanitizeSettings.tsx
Normal file
51
frontend/src/components/tools/sanitize/SanitizeSettings.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Stack, Text, Checkbox } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { SanitizeParameters, defaultParameters } from "../../../hooks/tools/sanitize/useSanitizeParameters";
|
||||||
|
|
||||||
|
interface SanitizeSettingsProps {
|
||||||
|
parameters: SanitizeParameters;
|
||||||
|
onParameterChange: (key: keyof SanitizeParameters, value: boolean) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SanitizeSettings = ({ parameters, onParameterChange, disabled = false }: SanitizeSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const options = (Object.keys(defaultParameters) as Array<keyof SanitizeParameters>).map((key) => ({
|
||||||
|
key: key,
|
||||||
|
label: t(`sanitize.options.${key}`, key),
|
||||||
|
description: t(`sanitize.options.${key}.desc`, `${key} from the PDF`),
|
||||||
|
default: defaultParameters[key],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{t('sanitize.options.title', 'Sanitization Options')}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Stack gap="sm">
|
||||||
|
{options.map((option) => (
|
||||||
|
<Checkbox
|
||||||
|
key={option.key}
|
||||||
|
checked={parameters[option.key]}
|
||||||
|
onChange={(event) => onParameterChange(option.key, event.currentTarget.checked)}
|
||||||
|
disabled={disabled}
|
||||||
|
label={
|
||||||
|
<div>
|
||||||
|
<Text size="sm">{option.label}</Text>
|
||||||
|
<Text size="xs" c="dimmed">{option.description}</Text>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t('sanitize.options.note', 'Select the elements you want to remove from the PDF. At least one option must be selected.')}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SanitizeSettings;
|
@ -38,12 +38,6 @@ export const useCompressOperation = () => {
|
|||||||
buildFormData,
|
buildFormData,
|
||||||
filePrefix: 'compressed_',
|
filePrefix: 'compressed_',
|
||||||
multiFileEndpoint: false, // Individual API calls per file
|
multiFileEndpoint: false, // Individual API calls per file
|
||||||
validateParams: (params) => {
|
|
||||||
if (params.compressionMethod === 'filesize' && !params.fileSizeValue) {
|
|
||||||
return { valid: false, errors: [t('compress.validation.fileSizeRequired', 'File size value is required when using filesize method')] };
|
|
||||||
}
|
|
||||||
return { valid: true };
|
|
||||||
},
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('compress.error.failed', 'An error occurred while compressing the PDF.'))
|
getErrorMessage: createStandardErrorHandler(t('compress.error.failed', 'An error occurred while compressing the PDF.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -134,9 +134,6 @@ export const useConvertOperation = () => {
|
|||||||
buildFormData, // Not used with customProcessor but required
|
buildFormData, // Not used with customProcessor but required
|
||||||
filePrefix: 'converted_',
|
filePrefix: 'converted_',
|
||||||
customProcessor: customConvertProcessor, // Convert handles its own routing
|
customProcessor: customConvertProcessor, // Convert handles its own routing
|
||||||
validateParams: (params) => {
|
|
||||||
return { valid: true };
|
|
||||||
},
|
|
||||||
getErrorMessage: (error) => {
|
getErrorMessage: (error) => {
|
||||||
if (error.response?.data && typeof error.response.data === 'string') {
|
if (error.response?.data && typeof error.response.data === 'string') {
|
||||||
return error.response.data;
|
return error.response.data;
|
||||||
|
@ -103,10 +103,6 @@ export const useOCROperation = () => {
|
|||||||
filePrefix: 'ocr_',
|
filePrefix: 'ocr_',
|
||||||
multiFileEndpoint: false, // Process files individually
|
multiFileEndpoint: false, // Process files individually
|
||||||
responseHandler, // use shared flow
|
responseHandler, // use shared flow
|
||||||
validateParams: (params) =>
|
|
||||||
params.languages.length === 0
|
|
||||||
? { valid: false, errors: [t('ocr.validation.languageRequired', 'Please select at least one language for OCR processing.')] }
|
|
||||||
: { valid: true },
|
|
||||||
getErrorMessage: (error) =>
|
getErrorMessage: (error) =>
|
||||||
error.message?.includes('OCR tools') && error.message?.includes('not installed')
|
error.message?.includes('OCR tools') && error.message?.includes('not installed')
|
||||||
? 'OCR tools (OCRmyPDF or Tesseract) are not installed on the server. Use the standard or fat Docker image instead of ultra-lite, or install OCR tools manually.'
|
? 'OCR tools (OCRmyPDF or Tesseract) are not installed on the server. Use the standard or fat Docker image instead of ultra-lite, or install OCR tools manually.'
|
||||||
|
32
frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts
Normal file
32
frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
|
import { SanitizeParameters } from './useSanitizeParameters';
|
||||||
|
|
||||||
|
const buildFormData = (parameters: SanitizeParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('fileInput', file);
|
||||||
|
|
||||||
|
// Add parameters
|
||||||
|
formData.append('removeJavaScript', parameters.removeJavaScript.toString());
|
||||||
|
formData.append('removeEmbeddedFiles', parameters.removeEmbeddedFiles.toString());
|
||||||
|
formData.append('removeXMPMetadata', parameters.removeXMPMetadata.toString());
|
||||||
|
formData.append('removeMetadata', parameters.removeMetadata.toString());
|
||||||
|
formData.append('removeLinks', parameters.removeLinks.toString());
|
||||||
|
formData.append('removeFonts', parameters.removeFonts.toString());
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSanitizeOperation = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return useToolOperation<SanitizeParameters>({
|
||||||
|
operationType: 'sanitize',
|
||||||
|
endpoint: '/api/v1/security/sanitize-pdf',
|
||||||
|
buildFormData,
|
||||||
|
filePrefix: t('sanitize.filenamePrefix', 'sanitized') + '_',
|
||||||
|
multiFileEndpoint: false, // Individual API calls per file
|
||||||
|
getErrorMessage: createStandardErrorHandler(t('sanitize.error.failed', 'An error occurred while sanitising the PDF.'))
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,90 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import { renderHook, act } from '@testing-library/react';
|
||||||
|
import { defaultParameters, useSanitizeParameters } from './useSanitizeParameters';
|
||||||
|
|
||||||
|
describe('useSanitizeParameters', () => {
|
||||||
|
test('should initialize with default parameters', () => {
|
||||||
|
const { result } = renderHook(() => useSanitizeParameters());
|
||||||
|
|
||||||
|
expect(result.current.parameters).toStrictEqual(defaultParameters);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update individual parameters', () => {
|
||||||
|
const { result } = renderHook(() => useSanitizeParameters());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('removeXMPMetadata', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters).toStrictEqual({
|
||||||
|
...defaultParameters, // Other params unchanged
|
||||||
|
removeXMPMetadata: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reset parameters to defaults', () => {
|
||||||
|
const { result } = renderHook(() => useSanitizeParameters());
|
||||||
|
|
||||||
|
// First, change some parameters
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('removeXMPMetadata', true);
|
||||||
|
result.current.updateParameter('removeJavaScript', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.removeXMPMetadata).toBe(true);
|
||||||
|
expect(result.current.parameters.removeJavaScript).toBe(false);
|
||||||
|
|
||||||
|
// Then reset
|
||||||
|
act(() => {
|
||||||
|
result.current.resetParameters();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters).toStrictEqual(defaultParameters);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return correct endpoint name', () => {
|
||||||
|
const { result } = renderHook(() => useSanitizeParameters());
|
||||||
|
|
||||||
|
expect(result.current.getEndpointName()).toBe('sanitize-pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should validate parameters correctly', () => {
|
||||||
|
const { result } = renderHook(() => useSanitizeParameters());
|
||||||
|
|
||||||
|
// Default state should be valid (has removeJavaScript and removeEmbeddedFiles enabled)
|
||||||
|
expect(result.current.validateParameters()).toBe(true);
|
||||||
|
|
||||||
|
// Turn off all parameters - should be invalid
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('removeJavaScript', false);
|
||||||
|
result.current.updateParameter('removeEmbeddedFiles', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.validateParameters()).toBe(false);
|
||||||
|
|
||||||
|
// Turn on one parameter - should be valid again
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('removeLinks', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.validateParameters()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle all parameter types correctly', () => {
|
||||||
|
const { result } = renderHook(() => useSanitizeParameters());
|
||||||
|
|
||||||
|
const allParameters = Object.keys(defaultParameters) as (keyof typeof defaultParameters)[];
|
||||||
|
|
||||||
|
allParameters.forEach(param => {
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter(param, true);
|
||||||
|
});
|
||||||
|
expect(result.current.parameters[param]).toBe(true);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter(param, false);
|
||||||
|
});
|
||||||
|
expect(result.current.parameters[param]).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
53
frontend/src/hooks/tools/sanitize/useSanitizeParameters.ts
Normal file
53
frontend/src/hooks/tools/sanitize/useSanitizeParameters.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
export interface SanitizeParameters {
|
||||||
|
removeJavaScript: boolean;
|
||||||
|
removeEmbeddedFiles: boolean;
|
||||||
|
removeXMPMetadata: boolean;
|
||||||
|
removeMetadata: boolean;
|
||||||
|
removeLinks: boolean;
|
||||||
|
removeFonts: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultParameters: SanitizeParameters = {
|
||||||
|
removeJavaScript: true,
|
||||||
|
removeEmbeddedFiles: true,
|
||||||
|
removeXMPMetadata: false,
|
||||||
|
removeMetadata: false,
|
||||||
|
removeLinks: false,
|
||||||
|
removeFonts: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSanitizeParameters = () => {
|
||||||
|
const [parameters, setParameters] = useState<SanitizeParameters>(defaultParameters);
|
||||||
|
|
||||||
|
const updateParameter = useCallback(<K extends keyof SanitizeParameters>(
|
||||||
|
key: K,
|
||||||
|
value: SanitizeParameters[K]
|
||||||
|
) => {
|
||||||
|
setParameters(prev => ({
|
||||||
|
...prev,
|
||||||
|
[key]: value
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetParameters = useCallback(() => {
|
||||||
|
setParameters(defaultParameters);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const validateParameters = useCallback(() => {
|
||||||
|
return Object.values(parameters).some(value => value === true);
|
||||||
|
}, [parameters]);
|
||||||
|
|
||||||
|
const getEndpointName = () => {
|
||||||
|
return 'sanitize-pdf'
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
parameters,
|
||||||
|
updateParameter,
|
||||||
|
resetParameters,
|
||||||
|
validateParameters,
|
||||||
|
getEndpointName,
|
||||||
|
};
|
||||||
|
};
|
@ -9,11 +9,6 @@ import { extractErrorMessage } from '../../../utils/toolErrorHandler';
|
|||||||
import { createOperation } from '../../../utils/toolOperationTracker';
|
import { createOperation } from '../../../utils/toolOperationTracker';
|
||||||
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
|
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
|
||||||
|
|
||||||
export interface ValidationResult {
|
|
||||||
valid: boolean;
|
|
||||||
errors?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-export for backwards compatibility
|
// Re-export for backwards compatibility
|
||||||
export type { ProcessingProgress, ResponseHandler };
|
export type { ProcessingProgress, ResponseHandler };
|
||||||
|
|
||||||
@ -64,9 +59,6 @@ export interface ToolOperationConfig<TParams = void> {
|
|||||||
*/
|
*/
|
||||||
customProcessor?: (params: TParams, files: File[]) => Promise<File[]>;
|
customProcessor?: (params: TParams, files: File[]) => Promise<File[]>;
|
||||||
|
|
||||||
/** Validate parameters before execution. Return validation errors if invalid. */
|
|
||||||
validateParams?: (params: TParams) => ValidationResult;
|
|
||||||
|
|
||||||
/** Extract user-friendly error messages from API errors */
|
/** Extract user-friendly error messages from API errors */
|
||||||
getErrorMessage?: (error: any) => string;
|
getErrorMessage?: (error: any) => string;
|
||||||
}
|
}
|
||||||
@ -129,14 +121,6 @@ export const useToolOperation = <TParams = void>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.validateParams) {
|
|
||||||
const validation = config.validateParams(params);
|
|
||||||
if (!validation.valid) {
|
|
||||||
actions.setError(validation.errors?.join(', ') || 'Invalid parameters');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validFiles = selectedFiles.filter(file => file.size > 0);
|
const validFiles = selectedFiles.filter(file => file.size > 0);
|
||||||
if (validFiles.length === 0) {
|
if (validFiles.length === 0) {
|
||||||
actions.setError(t('noValidFiles', 'No valid files to process'));
|
actions.setError(t('noValidFiles', 'No valid files to process'));
|
||||||
@ -186,7 +170,7 @@ export const useToolOperation = <TParams = void>(
|
|||||||
// Individual file processing - separate API call per file
|
// Individual file processing - separate API call per file
|
||||||
const apiCallsConfig: ApiCallsConfig<TParams> = {
|
const apiCallsConfig: ApiCallsConfig<TParams> = {
|
||||||
endpoint: config.endpoint,
|
endpoint: config.endpoint,
|
||||||
buildFormData: (file: File, params: TParams) => (config.buildFormData as any /* FIX ME */)(file, params),
|
buildFormData: (file: File, params: TParams) => (config.buildFormData as (params: TParams, file: File) => FormData /* FIX ME */)(params, file),
|
||||||
filePrefix: config.filePrefix,
|
filePrefix: config.filePrefix,
|
||||||
responseHandler: config.responseHandler
|
responseHandler: config.responseHandler
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation';
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import { SplitParameters } from '../../../components/tools/split/SplitSettings';
|
import { SplitParameters } from '../../../components/tools/split/SplitSettings';
|
||||||
import { SPLIT_MODES } from '../../../constants/splitConstants';
|
import { SPLIT_MODES } from '../../../constants/splitConstants';
|
||||||
@ -66,17 +64,6 @@ export const useSplitOperation = () => {
|
|||||||
buildFormData: buildFormData, // Multi-file signature: (params, selectedFiles) => FormData
|
buildFormData: buildFormData, // Multi-file signature: (params, selectedFiles) => FormData
|
||||||
filePrefix: 'split_',
|
filePrefix: 'split_',
|
||||||
multiFileEndpoint: true, // Single API call with all files
|
multiFileEndpoint: true, // Single API call with all files
|
||||||
validateParams: (params) => {
|
|
||||||
if (!params.mode) {
|
|
||||||
return { valid: false, errors: [t('split.validation.modeRequired', 'Split mode is required')] };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.mode === SPLIT_MODES.BY_PAGES && !params.pages) {
|
|
||||||
return { valid: false, errors: [t('split.validation.pagesRequired', 'Page numbers are required for split by pages')] };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { valid: true };
|
|
||||||
},
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('split.error.failed', 'An error occurred while splitting the PDF.'))
|
getErrorMessage: createStandardErrorHandler(t('split.error.failed', 'An error occurred while splitting the PDF.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { SPLIT_MODES, SPLIT_TYPES, ENDPOINTS, type SplitMode, type SplitType } from '../../../constants/splitConstants';
|
import { SPLIT_MODES, SPLIT_TYPES, ENDPOINTS, type SplitMode } from '../../../constants/splitConstants';
|
||||||
import { SplitParameters } from '../../../components/tools/split/SplitSettings';
|
import { SplitParameters } from '../../../components/tools/split/SplitSettings';
|
||||||
|
|
||||||
export interface SplitParametersHook {
|
export interface SplitParametersHook {
|
||||||
|
@ -4,6 +4,7 @@ import ContentCutIcon from "@mui/icons-material/ContentCut";
|
|||||||
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
||||||
import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
|
import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
|
||||||
import ApiIcon from "@mui/icons-material/Api";
|
import ApiIcon from "@mui/icons-material/Api";
|
||||||
|
import CleaningServicesIcon from "@mui/icons-material/CleaningServices";
|
||||||
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
||||||
import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool";
|
import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool";
|
||||||
|
|
||||||
@ -75,6 +76,15 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|||||||
description: "Extract text from images using OCR",
|
description: "Extract text from images using OCR",
|
||||||
endpoints: ["ocr-pdf"]
|
endpoints: ["ocr-pdf"]
|
||||||
},
|
},
|
||||||
|
sanitize: {
|
||||||
|
id: "sanitize",
|
||||||
|
icon: <CleaningServicesIcon />,
|
||||||
|
component: React.lazy(() => import("../tools/Sanitize")),
|
||||||
|
maxFiles: -1,
|
||||||
|
category: "security",
|
||||||
|
description: "Remove potentially harmful elements from PDF files",
|
||||||
|
endpoints: ["sanitize-pdf"]
|
||||||
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
166
frontend/src/tools/Sanitize.tsx
Normal file
166
frontend/src/tools/Sanitize.tsx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Button, Stack, Text } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
|
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||||
|
|
||||||
|
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||||
|
import OperationButton from "../components/tools/shared/OperationButton";
|
||||||
|
import ErrorNotification from "../components/tools/shared/ErrorNotification";
|
||||||
|
import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator";
|
||||||
|
import ResultsPreview from "../components/tools/shared/ResultsPreview";
|
||||||
|
import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings";
|
||||||
|
|
||||||
|
import { useSanitizeParameters } from "../hooks/tools/sanitize/useSanitizeParameters";
|
||||||
|
import { useSanitizeOperation } from "../hooks/tools/sanitize/useSanitizeOperation";
|
||||||
|
import { BaseToolProps } from "../types/tool";
|
||||||
|
import { useFileContext } from "../contexts/FileContext";
|
||||||
|
|
||||||
|
const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { selectedFiles } = useToolFileSelection();
|
||||||
|
const { setCurrentMode } = useFileContext();
|
||||||
|
|
||||||
|
const sanitizeParams = useSanitizeParameters();
|
||||||
|
const sanitizeOperation = useSanitizeOperation();
|
||||||
|
|
||||||
|
// Endpoint validation
|
||||||
|
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(
|
||||||
|
sanitizeParams.getEndpointName()
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sanitizeOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
}, [sanitizeParams.parameters, selectedFiles]);
|
||||||
|
|
||||||
|
const handleSanitize = async () => {
|
||||||
|
try {
|
||||||
|
await sanitizeOperation.executeOperation(
|
||||||
|
sanitizeParams.parameters,
|
||||||
|
selectedFiles,
|
||||||
|
);
|
||||||
|
if (sanitizeOperation.files && onComplete) {
|
||||||
|
onComplete(sanitizeOperation.files);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (onError) {
|
||||||
|
onError(error instanceof Error ? error.message : t('sanitize.error.generic', 'Sanitization failed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSettingsReset = () => {
|
||||||
|
sanitizeOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThumbnailClick = (file: File) => {
|
||||||
|
onPreviewFile?.(file);
|
||||||
|
sessionStorage.setItem('previousMode', 'sanitize');
|
||||||
|
setCurrentMode('viewer');
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasFiles = selectedFiles.length > 0;
|
||||||
|
const hasResults = sanitizeOperation.files.length > 0;
|
||||||
|
const filesCollapsed = hasFiles;
|
||||||
|
const settingsCollapsed = hasResults;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolStepContainer>
|
||||||
|
<Stack gap="sm" p="sm" style={{ height: '100vh', overflow: 'auto' }}>
|
||||||
|
{/* Files Step */}
|
||||||
|
<ToolStep
|
||||||
|
title={t('sanitize.steps.files', 'Files')}
|
||||||
|
isVisible={true}
|
||||||
|
isCollapsed={filesCollapsed}
|
||||||
|
isCompleted={filesCollapsed}
|
||||||
|
completedMessage={hasFiles ?
|
||||||
|
selectedFiles.length === 1
|
||||||
|
? t('fileSelected', 'Selected: {{filename}}', { filename: selectedFiles[0].name })
|
||||||
|
: t('filesSelected', 'Selected: {{count}} files', { count: selectedFiles.length })
|
||||||
|
: undefined}
|
||||||
|
>
|
||||||
|
<FileStatusIndicator
|
||||||
|
selectedFiles={selectedFiles}
|
||||||
|
placeholder={t('sanitize.files.placeholder', 'Select a PDF file in the main view to get started')}
|
||||||
|
/>
|
||||||
|
</ToolStep>
|
||||||
|
|
||||||
|
{/* Settings Step */}
|
||||||
|
<ToolStep
|
||||||
|
title={t('sanitize.steps.settings', 'Settings')}
|
||||||
|
isVisible={hasFiles}
|
||||||
|
isCollapsed={settingsCollapsed}
|
||||||
|
isCompleted={settingsCollapsed}
|
||||||
|
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
|
||||||
|
completedMessage={settingsCollapsed ? t('sanitize.completed', 'Sanitization completed') : undefined}
|
||||||
|
>
|
||||||
|
<Stack gap="sm">
|
||||||
|
<SanitizeSettings
|
||||||
|
parameters={sanitizeParams.parameters}
|
||||||
|
onParameterChange={sanitizeParams.updateParameter}
|
||||||
|
disabled={endpointLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OperationButton
|
||||||
|
onClick={handleSanitize}
|
||||||
|
isLoading={sanitizeOperation.isLoading}
|
||||||
|
disabled={!sanitizeParams.validateParameters() || !hasFiles || !endpointEnabled}
|
||||||
|
loadingText={t("loading")}
|
||||||
|
submitText={t("sanitize.submit", "Sanitize PDF")}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</ToolStep>
|
||||||
|
|
||||||
|
{/* Results Step */}
|
||||||
|
<ToolStep
|
||||||
|
title={t('sanitize.steps.results', 'Results')}
|
||||||
|
isVisible={hasResults}
|
||||||
|
>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{sanitizeOperation.status && (
|
||||||
|
<Text size="sm" c="dimmed">{sanitizeOperation.status}</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ErrorNotification
|
||||||
|
error={sanitizeOperation.errorMessage}
|
||||||
|
onClose={sanitizeOperation.clearError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{sanitizeOperation.downloadUrl && (
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
href={sanitizeOperation.downloadUrl}
|
||||||
|
download={sanitizeOperation.downloadFilename}
|
||||||
|
leftSection={<DownloadIcon />}
|
||||||
|
color="green"
|
||||||
|
fullWidth
|
||||||
|
mb="md"
|
||||||
|
>
|
||||||
|
{sanitizeOperation.files.length === 1
|
||||||
|
? t("download", "Download")
|
||||||
|
: t("downloadZip", "Download ZIP")
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ResultsPreview
|
||||||
|
files={sanitizeOperation.files.map((file, index) => ({
|
||||||
|
file,
|
||||||
|
thumbnail: sanitizeOperation.thumbnails[index]
|
||||||
|
}))}
|
||||||
|
onFileClick={handleThumbnailClick}
|
||||||
|
isGeneratingThumbnails={sanitizeOperation.isGeneratingThumbnails}
|
||||||
|
title={t("sanitize.sanitizationResults", "Sanitization Results")}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</ToolStep>
|
||||||
|
</Stack>
|
||||||
|
</ToolStepContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sanitize;
|
@ -11,7 +11,7 @@ export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';
|
|||||||
|
|
||||||
export type ToolType = 'merge' | 'split' | 'compress' | 'ocr' | 'convert' | 'sanitize';
|
export type ToolType = 'merge' | 'split' | 'compress' | 'ocr' | 'convert' | 'sanitize';
|
||||||
|
|
||||||
export type OperationType = 'merge' | 'split' | 'compress' | 'add' | 'remove' | 'replace' | 'convert' | 'upload' | 'ocr';
|
export type OperationType = 'merge' | 'split' | 'compress' | 'add' | 'remove' | 'replace' | 'convert' | 'upload' | 'ocr' | 'sanitize';
|
||||||
|
|
||||||
export interface FileOperation {
|
export interface FileOperation {
|
||||||
id: string;
|
id: string;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user