mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-08-11 13:48:37 +02:00
Merge cf2250be86
into 768ece6921
This commit is contained in:
commit
9fd70d2644
@ -38,7 +38,8 @@
|
||||
"save": "Save",
|
||||
"saveToBrowser": "Save to Browser",
|
||||
"close": "Close",
|
||||
"filesSelected": "files selected",
|
||||
"fileSelected": "Selected: {{filename}}",
|
||||
"filesSelected": "{{count}} files selected",
|
||||
"noFavourites": "No favourites added",
|
||||
"downloadComplete": "Download Complete",
|
||||
"bored": "Bored Waiting?",
|
||||
@ -391,6 +392,10 @@
|
||||
"title": "Compress",
|
||||
"desc": "Compress PDFs to reduce their file size."
|
||||
},
|
||||
"sanitize": {
|
||||
"title": "Sanitise",
|
||||
"desc": "Remove potentially harmful elements from PDF files."
|
||||
},
|
||||
"unlockPDFForms": {
|
||||
"title": "Unlock PDF Forms",
|
||||
"desc": "Remove read-only property of form fields in a PDF document."
|
||||
@ -1761,5 +1766,38 @@
|
||||
"fileTooLarge": "File too large. Maximum size per file is",
|
||||
"storageQuotaExceeded": "Storage quota exceeded. Please remove some files before uploading more.",
|
||||
"approximateSize": "Approximate size"
|
||||
},
|
||||
"sanitize": {
|
||||
"submit": "Sanitise PDF",
|
||||
"processing": "Sanitising PDF...",
|
||||
"completed": "Sanitisation completed successfully",
|
||||
"error": "Sanitisation failed: {{error}}",
|
||||
"error.generic": "Sanitisation failed",
|
||||
"filenamePrefix": "sanitised",
|
||||
"steps": {
|
||||
"files": "Files",
|
||||
"settings": "Settings",
|
||||
"results": "Results"
|
||||
},
|
||||
"files": {
|
||||
"selected": "Selected: {{filename}}",
|
||||
"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",
|
||||
"saveToBrowser": "Save to Browser",
|
||||
"close": "Close",
|
||||
"filesSelected": "files selected",
|
||||
"fileSelected": "Selected: {{filename}}",
|
||||
"filesSelected": "{{count}} files selected",
|
||||
"noFavourites": "No favorites added",
|
||||
"downloadComplete": "Download Complete",
|
||||
"bored": "Bored Waiting?",
|
||||
@ -387,6 +388,10 @@
|
||||
"title": "Compress",
|
||||
"desc": "Compress PDFs to reduce their file size."
|
||||
},
|
||||
"sanitize": {
|
||||
"title": "Sanitize",
|
||||
"desc": "Remove potentially harmful elements from PDF files."
|
||||
},
|
||||
"unlockPDFForms": {
|
||||
"title": "Unlock PDF Forms",
|
||||
"desc": "Remove read-only property of form fields in a PDF document."
|
||||
@ -1612,6 +1617,39 @@
|
||||
"pdfaOptions": "PDF/A Options",
|
||||
"outputFormat": "Output Format",
|
||||
"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",
|
||||
"processing": "Sanitizing PDF...",
|
||||
"completed": "Sanitization completed successfully",
|
||||
"error": "Sanitization failed: {{error}}",
|
||||
"error.generic": "Sanitization failed",
|
||||
"filenamePrefix": "sanitized",
|
||||
"steps": {
|
||||
"files": "Files",
|
||||
"settings": "Settings",
|
||||
"results": "Results"
|
||||
},
|
||||
"files": {
|
||||
"selected": "Selected: {{filename}}",
|
||||
"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);
|
||||
});
|
||||
});
|
83
frontend/src/components/tools/sanitize/SanitizeSettings.tsx
Normal file
83
frontend/src/components/tools/sanitize/SanitizeSettings.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { Stack, Text, Checkbox } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SanitizeParameters } 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 = [
|
||||
{
|
||||
key: 'removeJavaScript' as const,
|
||||
label: t('sanitize.options.removeJavaScript', 'Remove JavaScript'),
|
||||
description: t('sanitize.options.removeJavaScript.desc', 'Remove JavaScript actions and scripts from the PDF'),
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: 'removeEmbeddedFiles' as const,
|
||||
label: t('sanitize.options.removeEmbeddedFiles', 'Remove Embedded Files'),
|
||||
description: t('sanitize.options.removeEmbeddedFiles.desc', 'Remove any files embedded within the PDF'),
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: 'removeXMPMetadata' as const,
|
||||
label: t('sanitize.options.removeXMPMetadata', 'Remove XMP Metadata'),
|
||||
description: t('sanitize.options.removeXMPMetadata.desc', 'Remove XMP metadata from the PDF'),
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
key: 'removeMetadata' as const,
|
||||
label: t('sanitize.options.removeMetadata', 'Remove Document Metadata'),
|
||||
description: t('sanitize.options.removeMetadata.desc', 'Remove document information metadata (title, author, etc.)'),
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
key: 'removeLinks' as const,
|
||||
label: t('sanitize.options.removeLinks', 'Remove Links'),
|
||||
description: t('sanitize.options.removeLinks.desc', 'Remove external links and launch actions from the PDF'),
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
key: 'removeFonts' as const,
|
||||
label: t('sanitize.options.removeFonts', 'Remove Fonts'),
|
||||
description: t('sanitize.options.removeFonts.desc', 'Remove embedded fonts from the PDF'),
|
||||
default: false,
|
||||
},
|
||||
];
|
||||
|
||||
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;
|
237
frontend/src/hooks/tools/sanitize/useSanitizeOperation.test.ts
Normal file
237
frontend/src/hooks/tools/sanitize/useSanitizeOperation.test.ts
Normal file
@ -0,0 +1,237 @@
|
||||
import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useSanitizeOperation } from './useSanitizeOperation';
|
||||
|
||||
// Mock useTranslation
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, fallback?: string, options?: any) => {
|
||||
if (key === 'sanitize.error' && options?.error) {
|
||||
return `Sanitization failed: ${options.error}`;
|
||||
}
|
||||
if (key === 'error.noFilesSelected') {
|
||||
return 'No files selected';
|
||||
}
|
||||
if (key === 'sanitize.error.generic') {
|
||||
return 'Sanitization failed';
|
||||
}
|
||||
return fallback || key;
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
// Mock FileContext
|
||||
vi.mock('../../../contexts/FileContext', () => ({
|
||||
useFileContext: () => ({
|
||||
recordOperation: vi.fn(),
|
||||
markOperationApplied: vi.fn(),
|
||||
markOperationFailed: vi.fn(),
|
||||
addFiles: vi.fn()
|
||||
})
|
||||
}));
|
||||
|
||||
// Mock fetch
|
||||
const mockFetch = vi.fn();
|
||||
vi.stubGlobal('fetch', mockFetch);
|
||||
|
||||
// Mock URL.createObjectURL and revokeObjectURL
|
||||
const mockCreateObjectURL = vi.fn(() => 'mock-blob-url');
|
||||
const mockRevokeObjectURL = vi.fn();
|
||||
vi.stubGlobal('URL', {
|
||||
createObjectURL: mockCreateObjectURL,
|
||||
revokeObjectURL: mockRevokeObjectURL
|
||||
});
|
||||
|
||||
describe('useSanitizeOperation', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should initialize with default state', () => {
|
||||
const { result } = renderHook(() => useSanitizeOperation());
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.errorMessage).toBe(null);
|
||||
expect(result.current.downloadUrl).toBe(null);
|
||||
expect(result.current.status).toBe(null);
|
||||
});
|
||||
|
||||
test('should execute sanitization operation successfully', async () => {
|
||||
const mockBlob = new Blob(['test'], { type: 'application/pdf' });
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
blob: () => Promise.resolve(mockBlob)
|
||||
};
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useSanitizeOperation());
|
||||
|
||||
const parameters = {
|
||||
removeJavaScript: true,
|
||||
removeEmbeddedFiles: false,
|
||||
removeXMPMetadata: true,
|
||||
removeMetadata: false,
|
||||
removeLinks: false,
|
||||
removeFonts: false
|
||||
};
|
||||
|
||||
const testFile = new File(['test'], 'test.pdf', { type: 'application/pdf' });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.executeOperation(parameters, [testFile]);
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/api/v1/security/sanitize-pdf', {
|
||||
method: 'POST',
|
||||
body: expect.any(FormData)
|
||||
});
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.downloadUrl).toBe('mock-blob-url');
|
||||
expect(result.current.status).toBe('Sanitization completed successfully');
|
||||
expect(result.current.errorMessage).toBe(null);
|
||||
});
|
||||
|
||||
test('should handle API errors correctly', async () => {
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
text: () => Promise.resolve('Server error')
|
||||
};
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useSanitizeOperation());
|
||||
|
||||
const parameters = {
|
||||
removeJavaScript: true,
|
||||
removeEmbeddedFiles: true,
|
||||
removeXMPMetadata: false,
|
||||
removeMetadata: false,
|
||||
removeLinks: false,
|
||||
removeFonts: false
|
||||
};
|
||||
|
||||
const testFile = new File(['test'], 'test.pdf', { type: 'application/pdf' });
|
||||
|
||||
await act(async () => {
|
||||
await expect(result.current.executeOperation(parameters, [testFile]))
|
||||
.rejects.toThrow('Failed to sanitize all files: test.pdf');
|
||||
});
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.errorMessage).toBe('Failed to sanitize all files: test.pdf');
|
||||
expect(result.current.downloadUrl).toBe(null);
|
||||
expect(result.current.status).toBe(null);
|
||||
});
|
||||
|
||||
test('should handle no files selected error', async () => {
|
||||
const { result } = renderHook(() => useSanitizeOperation());
|
||||
|
||||
const parameters = {
|
||||
removeJavaScript: true,
|
||||
removeEmbeddedFiles: true,
|
||||
removeXMPMetadata: false,
|
||||
removeMetadata: false,
|
||||
removeLinks: false,
|
||||
removeFonts: false
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
await expect(result.current.executeOperation(parameters, []))
|
||||
.rejects.toThrow('No files selected');
|
||||
});
|
||||
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should send correct form data to API', async () => {
|
||||
const mockBlob = new Blob(['test'], { type: 'application/pdf' });
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
blob: () => Promise.resolve(mockBlob)
|
||||
};
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useSanitizeOperation());
|
||||
|
||||
const parameters = {
|
||||
removeJavaScript: true,
|
||||
removeEmbeddedFiles: false,
|
||||
removeXMPMetadata: true,
|
||||
removeMetadata: false,
|
||||
removeLinks: true,
|
||||
removeFonts: false
|
||||
};
|
||||
|
||||
const testFile = new File(['test'], 'test.pdf', { type: 'application/pdf' });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.executeOperation(parameters, [testFile]);
|
||||
});
|
||||
|
||||
const [url, options] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe('/api/v1/security/sanitize-pdf');
|
||||
expect(options.method).toBe('POST');
|
||||
|
||||
const formData = options.body as FormData;
|
||||
expect(formData.get('removeJavaScript')).toBe('true');
|
||||
expect(formData.get('removeEmbeddedFiles')).toBe('false');
|
||||
expect(formData.get('removeXMPMetadata')).toBe('true');
|
||||
expect(formData.get('removeMetadata')).toBe('false');
|
||||
expect(formData.get('removeLinks')).toBe('true');
|
||||
expect(formData.get('removeFonts')).toBe('false');
|
||||
expect(formData.get('fileInput')).toBe(testFile);
|
||||
});
|
||||
|
||||
test('should reset results correctly', () => {
|
||||
const { result } = renderHook(() => useSanitizeOperation());
|
||||
|
||||
act(() => {
|
||||
result.current.resetResults();
|
||||
});
|
||||
|
||||
expect(result.current.downloadUrl).toBe(null);
|
||||
expect(result.current.errorMessage).toBe(null);
|
||||
expect(result.current.status).toBe(null);
|
||||
expect(mockRevokeObjectURL).not.toHaveBeenCalled(); // No URL to revoke initially
|
||||
});
|
||||
|
||||
test('should clear error message', async () => {
|
||||
// Mock a failed API response
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
text: () => Promise.resolve('API Error')
|
||||
};
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useSanitizeOperation());
|
||||
|
||||
const parameters = {
|
||||
removeJavaScript: true,
|
||||
removeEmbeddedFiles: true,
|
||||
removeXMPMetadata: false,
|
||||
removeMetadata: false,
|
||||
removeLinks: false,
|
||||
removeFonts: false
|
||||
};
|
||||
|
||||
const testFile = new File(['test'], 'test.pdf', { type: 'application/pdf' });
|
||||
|
||||
// Trigger an API error
|
||||
await act(async () => {
|
||||
await expect(result.current.executeOperation(parameters, [testFile]))
|
||||
.rejects.toThrow('Failed to sanitize all files: test.pdf');
|
||||
});
|
||||
|
||||
expect(result.current.errorMessage).toBe('Failed to sanitize all files: test.pdf');
|
||||
|
||||
act(() => {
|
||||
result.current.clearError();
|
||||
});
|
||||
|
||||
expect(result.current.errorMessage).toBe(null);
|
||||
});
|
||||
});
|
268
frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts
Normal file
268
frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts
Normal file
@ -0,0 +1,268 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFileContext } from '../../../contexts/FileContext';
|
||||
import { FileOperation } from '../../../types/fileContext';
|
||||
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
|
||||
import { zipFileService } from '../../../services/zipFileService';
|
||||
import { SanitizeParameters } from './useSanitizeParameters';
|
||||
|
||||
export const useSanitizeOperation = () => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
recordOperation,
|
||||
markOperationApplied,
|
||||
markOperationFailed,
|
||||
addFiles
|
||||
} = useFileContext();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
||||
const [downloadFilename, setDownloadFilename] = useState<string>('');
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [thumbnails, setThumbnails] = useState<string[]>([]);
|
||||
const [isGeneratingThumbnails, setIsGeneratingThumbnails] = useState(false);
|
||||
|
||||
const createOperation = useCallback((
|
||||
parameters: SanitizeParameters,
|
||||
selectedFiles: File[]
|
||||
): { operation: FileOperation; operationId: string; fileId: string } => {
|
||||
const operationId = `sanitize-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const fileId = selectedFiles[0].name;
|
||||
|
||||
const operation: FileOperation = {
|
||||
id: operationId,
|
||||
type: 'sanitize',
|
||||
timestamp: Date.now(),
|
||||
fileIds: selectedFiles.map(f => f.name),
|
||||
status: 'pending',
|
||||
metadata: {
|
||||
originalFileName: selectedFiles[0].name,
|
||||
parameters: {
|
||||
removeJavaScript: parameters.removeJavaScript,
|
||||
removeEmbeddedFiles: parameters.removeEmbeddedFiles,
|
||||
removeXMPMetadata: parameters.removeXMPMetadata,
|
||||
removeMetadata: parameters.removeMetadata,
|
||||
removeLinks: parameters.removeLinks,
|
||||
removeFonts: parameters.removeFonts,
|
||||
},
|
||||
fileSize: selectedFiles[0].size
|
||||
}
|
||||
};
|
||||
|
||||
return { operation, operationId, fileId };
|
||||
}, []);
|
||||
|
||||
const buildFormData = useCallback((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;
|
||||
}, []);
|
||||
|
||||
const generateSanitizedFileName = (originalFileName: string): string => {
|
||||
const baseName = originalFileName.replace(/\.[^/.]+$/, '');
|
||||
const prefix = t('sanitize.filenamePrefix', 'sanitized');
|
||||
return `${prefix}_${baseName}.pdf`;
|
||||
};
|
||||
|
||||
const sanitizeFile = useCallback(async (
|
||||
file: File,
|
||||
parameters: SanitizeParameters,
|
||||
operationId: string,
|
||||
fileId: string
|
||||
): Promise<File | null> => {
|
||||
try {
|
||||
const formData = buildFormData(parameters, file);
|
||||
|
||||
const response = await fetch('/api/v1/security/sanitize-pdf', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
markOperationFailed(fileId, operationId, errorText);
|
||||
console.error(`Error sanitizing file ${file.name}:`, errorText);
|
||||
return null;
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const sanitizedFileName = generateSanitizedFileName(file.name);
|
||||
const sanitizedFile = new File([blob], sanitizedFileName, { type: blob.type });
|
||||
|
||||
markOperationApplied(fileId, operationId);
|
||||
return sanitizedFile;
|
||||
} catch (error) {
|
||||
console.error(`Error sanitizing file ${file.name}:`, error);
|
||||
markOperationFailed(fileId, operationId, error instanceof Error ? error.message : 'Unknown error');
|
||||
return null;
|
||||
}
|
||||
}, [buildFormData, markOperationApplied, markOperationFailed]);
|
||||
|
||||
const createDownloadInfo = useCallback(async (results: File[]): Promise<void> => {
|
||||
if (results.length === 1) {
|
||||
const url = window.URL.createObjectURL(results[0]);
|
||||
setDownloadUrl(url);
|
||||
setDownloadFilename(results[0].name);
|
||||
} else {
|
||||
const zipFilename = `${t('sanitize.filenamePrefix', 'sanitized')}_files.zip`;
|
||||
const { zipFile } = await zipFileService.createZipFromFiles(results, zipFilename);
|
||||
const url = window.URL.createObjectURL(zipFile);
|
||||
setDownloadUrl(url);
|
||||
setDownloadFilename(zipFilename);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const generateThumbnailsForResults = useCallback(async (results: File[]): Promise<void> => {
|
||||
const thumbnails = await Promise.all(
|
||||
results.map(async (file) => {
|
||||
try {
|
||||
const thumbnail = await generateThumbnailForFile(file);
|
||||
return thumbnail || '';
|
||||
} catch (error) {
|
||||
console.warn(`Failed to generate thumbnail for ${file.name}:`, error);
|
||||
return '';
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
setThumbnails(thumbnails);
|
||||
}, []);
|
||||
|
||||
const processResults = useCallback(async (results: File[]): Promise<void> => {
|
||||
setFiles(results);
|
||||
setIsGeneratingThumbnails(true);
|
||||
|
||||
// Add sanitized files to FileContext for future use
|
||||
await addFiles(results);
|
||||
|
||||
// Create download info - single file or ZIP
|
||||
await createDownloadInfo(results);
|
||||
|
||||
// Generate thumbnails
|
||||
await generateThumbnailsForResults(results);
|
||||
|
||||
setIsGeneratingThumbnails(false);
|
||||
setStatus(results.length === 1
|
||||
? t('sanitize.completed', 'Sanitization completed successfully')
|
||||
: t('sanitize.completedMultiple', 'Sanitized {{count}} files successfully', { count: results.length })
|
||||
);
|
||||
}, [addFiles, createDownloadInfo, generateThumbnailsForResults, t]);
|
||||
|
||||
const executeOperation = useCallback(async (
|
||||
parameters: SanitizeParameters,
|
||||
selectedFiles: File[],
|
||||
) => {
|
||||
if (selectedFiles.length === 0) {
|
||||
throw new Error(t('error.noFilesSelected', 'No files selected'));
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setErrorMessage(null);
|
||||
setStatus(selectedFiles.length === 1
|
||||
? t('sanitize.processing', 'Sanitizing PDF...')
|
||||
: t('sanitize.processingMultiple', 'Sanitizing {{count}} PDFs...', { count: selectedFiles.length })
|
||||
);
|
||||
|
||||
const results: File[] = [];
|
||||
const failedFiles: string[] = [];
|
||||
|
||||
try {
|
||||
// Process each file separately
|
||||
for (let i = 0; i < selectedFiles.length; i++) {
|
||||
const file = selectedFiles[i];
|
||||
const { operation, operationId, fileId } = createOperation(parameters, [file]);
|
||||
recordOperation(fileId, operation);
|
||||
|
||||
setStatus(selectedFiles.length === 1
|
||||
? t('sanitize.processing', 'Sanitizing PDF...')
|
||||
: t('sanitize.processingFile', 'Processing file {{current}} of {{total}}: {{filename}}', {
|
||||
current: i + 1,
|
||||
total: selectedFiles.length,
|
||||
filename: file.name
|
||||
})
|
||||
);
|
||||
|
||||
const sanitizedFile = await sanitizeFile(file, parameters, operationId, fileId);
|
||||
|
||||
if (sanitizedFile) {
|
||||
results.push(sanitizedFile);
|
||||
} else {
|
||||
failedFiles.push(file.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (failedFiles.length > 0 && results.length === 0) {
|
||||
throw new Error(`Failed to sanitize all files: ${failedFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
if (failedFiles.length > 0) {
|
||||
setStatus(`Sanitized ${results.length}/${selectedFiles.length} files. Failed: ${failedFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
if (results.length > 0) {
|
||||
await processResults(results);
|
||||
} else {
|
||||
setErrorMessage(t('sanitize.errorAllFilesFailed', 'All files failed to sanitize'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in sanitization operation:', error);
|
||||
const message = error instanceof Error ? error.message : t('sanitize.error.generic', 'Sanitization failed');
|
||||
setErrorMessage(message);
|
||||
setStatus(null);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [t, createOperation, recordOperation, sanitizeFile, processResults]);
|
||||
|
||||
const resetResults = useCallback(() => {
|
||||
if (downloadUrl) {
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
}
|
||||
setFiles([]);
|
||||
setThumbnails([]);
|
||||
setIsGeneratingThumbnails(false);
|
||||
setDownloadUrl(null);
|
||||
setDownloadFilename('');
|
||||
setErrorMessage(null);
|
||||
setStatus(null);
|
||||
}, [downloadUrl]);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setErrorMessage(null);
|
||||
}, []);
|
||||
|
||||
// Cleanup blob URLs on unmount to prevent memory leaks
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (downloadUrl) {
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
}
|
||||
};
|
||||
}, [downloadUrl]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
errorMessage,
|
||||
downloadUrl,
|
||||
downloadFilename,
|
||||
status,
|
||||
files,
|
||||
thumbnails,
|
||||
isGeneratingThumbnails,
|
||||
executeOperation,
|
||||
resetResults,
|
||||
clearError,
|
||||
};
|
||||
};
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
54
frontend/src/hooks/tools/sanitize/useSanitizeParameters.ts
Normal file
54
frontend/src/hooks/tools/sanitize/useSanitizeParameters.ts
Normal file
@ -0,0 +1,54 @@
|
||||
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 getEndpointName = () => {
|
||||
return 'sanitize-pdf'
|
||||
};
|
||||
|
||||
const validateParameters = useCallback(() => {
|
||||
// At least one sanitization option must be selected
|
||||
return Object.values(parameters).some(value => value === true);
|
||||
}, [parameters]);
|
||||
|
||||
return {
|
||||
parameters,
|
||||
updateParameter,
|
||||
resetParameters,
|
||||
getEndpointName,
|
||||
validateParameters,
|
||||
};
|
||||
};
|
@ -4,6 +4,7 @@ import ContentCutIcon from "@mui/icons-material/ContentCut";
|
||||
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
||||
import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
|
||||
import ApiIcon from "@mui/icons-material/Api";
|
||||
import CleaningServicesIcon from "@mui/icons-material/CleaningServices";
|
||||
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
||||
import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool";
|
||||
|
||||
@ -75,6 +76,15 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
||||
description: "Extract text from images using OCR",
|
||||
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"]
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
|
@ -122,7 +122,11 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
isVisible={true}
|
||||
isCollapsed={filesCollapsed}
|
||||
isCompleted={filesCollapsed}
|
||||
completedMessage={hasFiles ? `${selectedFiles.length} ${t("filesSelected", "files selected")}` : undefined}
|
||||
completedMessage={hasFiles ?
|
||||
selectedFiles.length === 1
|
||||
? t('fileSelected', 'Selected: {{filename}}', { filename: selectedFiles[0].name })
|
||||
: t('filesSelected', '{{count}} files selected', { count: selectedFiles.length })
|
||||
: undefined}
|
||||
>
|
||||
<FileStatusIndicator
|
||||
selectedFiles={selectedFiles}
|
||||
|
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: '80vh', 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 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 {
|
||||
id: string;
|
||||
|
Loading…
Reference in New Issue
Block a user