diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json
index 6e5dd6179..247162df3 100644
--- a/frontend/public/locales/en-GB/translation.json
+++ b/frontend/public/locales/en-GB/translation.json
@@ -384,6 +384,10 @@
"title": "Add Password",
"desc": "Encrypt your PDF document with a password."
},
+ "changePermissions": {
+ "title": "Change Permissions",
+ "desc": "Change document restrictions and permissions."
+ },
"removePassword": {
"title": "Remove Password",
"desc": "Remove password protection from your PDF document."
@@ -816,30 +820,6 @@
"removePages": {
"tags": "Remove pages,delete pages"
},
- "addPassword": {
- "tags": "secure,security",
- "title": "Add Password",
- "header": "Add password (Encrypt)",
- "selectText": {
- "1": "Select PDF to encrypt",
- "2": "User Password",
- "3": "Encryption Key Length",
- "4": "Higher values are stronger, but lower values have better compatibility.",
- "5": "Permissions to set (Recommended to be used along with Owner password)",
- "6": "Prevent assembly of document",
- "7": "Prevent content extraction",
- "8": "Prevent extraction for accessibility",
- "9": "Prevent filling in form",
- "10": "Prevent modification",
- "11": "Prevent annotation modification",
- "12": "Prevent printing",
- "13": "Prevent printing different formats",
- "14": "Owner Password",
- "15": "Restricts what can be done with the document once it is opened (Not supported by all readers)",
- "16": "Restricts the opening of the document itself"
- },
- "submit": "Encrypt"
- },
"removePassword": {
"tags": "secure,Decrypt,security,unpassword,delete password",
"title": "Remove password",
@@ -1798,5 +1778,109 @@
"removeFonts": "Remove Fonts",
"removeFonts.desc": "Remove embedded fonts from the PDF"
}
+ },
+ "addPassword": {
+ "completed": "Password protection applied",
+ "submit": "Encrypt",
+ "filenamePrefix": "encrypted",
+ "error": {
+ "failed": "An error occurred while encrypting the PDF."
+ },
+ "passwords": {
+ "title": "Passwords",
+ "stepTitle": "Passwords & Encryption",
+ "completed": "Passwords configured",
+ "user": {
+ "label": "User Password",
+ "placeholder": "Enter user password"
+ },
+ "owner": {
+ "label": "Owner Password",
+ "placeholder": "Enter owner password"
+ }
+ },
+ "permissions": {
+ "stepTitle": "Document Permissions"
+ },
+ "encryption": {
+ "title": "Encryption",
+ "keyLength": {
+ "label": "Key Length",
+ "40bit": "40-bit (Low)",
+ "128bit": "128-bit (Standard)",
+ "256bit": "256-bit (High)"
+ }
+ },
+ "results": {
+ "title": "Encrypted PDFs"
+ },
+ "tooltip": {
+ "header": {
+ "title": "Password Protection Overview"
+ },
+ "passwords": {
+ "title": "Password Types",
+ "text": "User passwords restrict opening the document, while owner passwords control what can be done with the document once opened. You can set both or just one.",
+ "bullet1": "User Password: Required to open the PDF",
+ "bullet2": "Owner Password: Controls document permissions (not supported by all PDF viewers)"
+ },
+ "encryption": {
+ "title": "Encryption Levels",
+ "text": "Higher encryption levels provide better security but may not be supported by older PDF viewers.",
+ "bullet1": "40-bit: Basic security, compatible with older viewers",
+ "bullet2": "128-bit: Standard security, widely supported",
+ "bullet3": "256-bit: Maximum security, requires modern viewers"
+ },
+ "restrictions": {
+ "title": "Document Restrictions",
+ "text": "These restrictions control what users can do with the PDF. Most effective when combined with an owner password."
+ }
+ }
+ },
+ "changePermissions": {
+ "completed": "Permissions changed",
+ "submit": "Change Permissions",
+ "error": {
+ "failed": "An error occurred while changing PDF permissions."
+ },
+ "restrictions": {
+ "title": "Document Restrictions",
+ "preventAssembly": {
+ "label": "Prevent assembly of document"
+ },
+ "preventExtractContent": {
+ "label": "Prevent content extraction"
+ },
+ "preventExtractForAccessibility": {
+ "label": "Prevent extraction for accessibility"
+ },
+ "preventFillInForm": {
+ "label": "Prevent filling in form"
+ },
+ "preventModify": {
+ "label": "Prevent modification"
+ },
+ "preventModifyAnnotations": {
+ "label": "Prevent annotation modification"
+ },
+ "preventPrinting": {
+ "label": "Prevent printing"
+ },
+ "preventPrintingFaithful": {
+ "label": "Prevent printing different formats"
+ }
+ },
+ "results": {
+ "title": "Modified PDFs"
+ },
+ "tooltip": {
+ "header": {
+ "title": "Change Permissions"
+ },
+ "description": {
+ "title": "Description",
+ "text": "Changes document permissions. Warning: To make these restrictions unchangeable, use the Add Password tool to set an owner password."
+ }
+ }
}
}
diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json
index 6ca67480b..641acf013 100644
--- a/frontend/public/locales/en-US/translation.json
+++ b/frontend/public/locales/en-US/translation.json
@@ -380,6 +380,10 @@
"title": "Add Password",
"desc": "Encrypt your PDF document with a password."
},
+ "changePermissions": {
+ "title": "Change Permissions",
+ "desc": "Change document restrictions and permissions."
+ },
"removePassword": {
"title": "Remove Password",
"desc": "Remove password protection from your PDF document."
@@ -745,30 +749,6 @@
"removePages": {
"tags": "Remove pages,delete pages"
},
- "addPassword": {
- "tags": "secure,security",
- "title": "Add Password",
- "header": "Add password (Encrypt)",
- "selectText": {
- "1": "Select PDF to encrypt",
- "2": "User Password",
- "3": "Encryption Key Length",
- "4": "Higher values are stronger, but lower values have better compatibility.",
- "5": "Permissions to set (Recommended to be used along with Owner password)",
- "6": "Prevent assembly of document",
- "7": "Prevent content extraction",
- "8": "Prevent extraction for accessibility",
- "9": "Prevent filling in form",
- "10": "Prevent modification",
- "11": "Prevent annotation modification",
- "12": "Prevent printing",
- "13": "Prevent printing different formats",
- "14": "Owner Password",
- "15": "Restricts what can be done with the document once it is opened (Not supported by all readers)",
- "16": "Restricts the opening of the document itself"
- },
- "submit": "Encrypt"
- },
"removePassword": {
"tags": "secure,Decrypt,security,unpassword,delete password",
"title": "Remove password",
@@ -1650,5 +1630,109 @@
"removeFonts.desc": "Remove embedded fonts from the PDF"
}
}
+ },
+ "addPassword": {
+ "completed": "Password protection applied",
+ "submit": "Encrypt",
+ "filenamePrefix": "encrypted",
+ "error": {
+ "failed": "An error occurred while encrypting the PDF."
+ },
+ "passwords": {
+ "title": "Passwords",
+ "stepTitle": "Passwords & Encryption",
+ "completed": "Passwords configured",
+ "user": {
+ "label": "User Password",
+ "placeholder": "Enter user password"
+ },
+ "owner": {
+ "label": "Owner Password",
+ "placeholder": "Enter owner password"
+ }
+ },
+ "permissions": {
+ "stepTitle": "Document Permissions"
+ },
+ "encryption": {
+ "title": "Encryption",
+ "keyLength": {
+ "label": "Key Length",
+ "40bit": "40-bit (Low)",
+ "128bit": "128-bit (Standard)",
+ "256bit": "256-bit (High)"
+ }
+ },
+ "results": {
+ "title": "Password Protected PDFs"
+ },
+ "tooltip": {
+ "header": {
+ "title": "Password Protection Overview"
+ },
+ "passwords": {
+ "title": "Password Types",
+ "text": "User passwords restrict opening the document, while owner passwords control what can be done with the document once opened. You can set both or just one.",
+ "bullet1": "User Password: Required to open the PDF",
+ "bullet2": "Owner Password: Controls document permissions (not supported by all PDF viewers)"
+ },
+ "encryption": {
+ "title": "Encryption Levels",
+ "text": "Higher encryption levels provide better security but may not be supported by older PDF viewers.",
+ "bullet1": "40-bit: Basic security, compatible with older viewers",
+ "bullet2": "128-bit: Standard security, widely supported",
+ "bullet3": "256-bit: Maximum security, requires modern viewers"
+ },
+ "restrictions": {
+ "title": "Document Restrictions",
+ "text": "These restrictions control what users can do with the PDF. Most effective when combined with an owner password."
+ }
+ }
+ },
+ "changePermissions": {
+ "completed": "Permissions changed",
+ "submit": "Change Permissions",
+ "error": {
+ "failed": "An error occurred while changing PDF permissions."
+ },
+ "restrictions": {
+ "title": "Document Restrictions",
+ "preventAssembly": {
+ "label": "Prevent assembly of document"
+ },
+ "preventExtractContent": {
+ "label": "Prevent content extraction"
+ },
+ "preventExtractForAccessibility": {
+ "label": "Prevent extraction for accessibility"
+ },
+ "preventFillInForm": {
+ "label": "Prevent filling in form"
+ },
+ "preventModify": {
+ "label": "Prevent modification"
+ },
+ "preventModifyAnnotations": {
+ "label": "Prevent annotation modification"
+ },
+ "preventPrinting": {
+ "label": "Prevent printing"
+ },
+ "preventPrintingFaithful": {
+ "label": "Prevent printing different formats"
+ }
+ },
+ "results": {
+ "title": "Modified PDFs"
+ },
+ "tooltip": {
+ "header": {
+ "title": "Change Permissions"
+ },
+ "description": {
+ "title": "Description",
+ "text": "Changes document permissions. Warning: To make these restrictions unchangeable, use the Add Password tool to set an owner password."
+ }
+ }
}
}
diff --git a/frontend/src/components/tools/addPassword/AddPasswordSettings.test.tsx b/frontend/src/components/tools/addPassword/AddPasswordSettings.test.tsx
new file mode 100644
index 000000000..ac4b000f2
--- /dev/null
+++ b/frontend/src/components/tools/addPassword/AddPasswordSettings.test.tsx
@@ -0,0 +1,179 @@
+import { describe, expect, test, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { MantineProvider } from '@mantine/core';
+import AddPasswordSettings from './AddPasswordSettings';
+import { defaultParameters } from '../../../hooks/tools/addPassword/useAddPasswordParameters';
+import type { AddPasswordParameters } from '../../../hooks/tools/addPassword/useAddPasswordParameters';
+
+// 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 }) => (
+ {children}
+);
+
+describe('AddPasswordSettings', () => {
+ const mockOnParameterChange = vi.fn();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ test('should render password input fields', () => {
+ render(
+
+
+
+ );
+
+ // Should render user and owner password fields labels
+ expect(screen.getByText('mock-addPassword.passwords.user.label')).toBeInTheDocument();
+ expect(screen.getByText('mock-addPassword.passwords.owner.label')).toBeInTheDocument();
+ });
+
+ test('should render encryption key length select', () => {
+ render(
+
+
+
+ );
+
+ // Should render key length select input
+ expect(screen.getByRole('textbox', { name: /keyLength/i })).toBeInTheDocument();
+ });
+
+ test('should render main component sections', () => {
+ render(
+
+
+
+ );
+
+ // Check that main section titles are rendered
+ expect(screen.getByText('mock-addPassword.passwords.title')).toBeInTheDocument();
+ expect(screen.getByText('mock-addPassword.encryption.title')).toBeInTheDocument();
+ });
+
+ test('should call onParameterChange when password fields are modified', () => {
+ render(
+
+
+
+ );
+
+ // This test is complex with Mantine's PasswordInput, just verify the component renders
+ expect(screen.getByText('mock-addPassword.passwords.user.label')).toBeInTheDocument();
+ });
+
+ test('should call onParameterChange when key length is changed', () => {
+ render(
+
+
+
+ );
+
+ // Find key length select and change it
+ const keyLengthSelect = screen.getByText('mock-addPassword.encryption.keyLength.128bit');
+
+ fireEvent.mouseDown(keyLengthSelect);
+ const option256 = screen.getByText('mock-addPassword.encryption.keyLength.256bit');
+ fireEvent.click(option256);
+
+ expect(mockOnParameterChange).toHaveBeenCalledWith('keyLength', 256);
+ });
+
+ test('should disable all form elements when disabled prop is true', () => {
+ render(
+
+
+
+ );
+
+ // Check password inputs are disabled
+ const passwordInputs = screen.getAllByRole('textbox');
+ passwordInputs.forEach(input => {
+ expect(input).toBeDisabled();
+ });
+
+ // Check key length select is disabled - simplified test due to Mantine complexity
+ expect(screen.getByText('mock-addPassword.encryption.keyLength.128bit')).toBeInTheDocument();
+ });
+
+ test('should enable all form elements when disabled prop is false', () => {
+ render(
+
+
+
+ );
+
+ // Check password inputs are enabled
+ const passwordInputs = screen.getAllByRole('textbox');
+ passwordInputs.forEach(input => {
+ expect(input).not.toBeDisabled();
+ });
+
+ // Check key length select is enabled - simplified test due to Mantine complexity
+ expect(screen.getByText('mock-addPassword.encryption.keyLength.128bit')).toBeInTheDocument();
+ });
+
+ test('should call translation function with correct keys', () => {
+ render(
+
+
+
+ );
+
+ // Verify that translation keys are being called
+ expect(mockT).toHaveBeenCalledWith('addPassword.passwords.title', 'Passwords');
+ expect(mockT).toHaveBeenCalledWith('addPassword.encryption.title', 'Encryption');
+ expect(mockT).toHaveBeenCalledWith('addPassword.passwords.user.label', 'User Password');
+ expect(mockT).toHaveBeenCalledWith('addPassword.passwords.owner.label', 'Owner Password');
+ });
+
+ test.each([
+ { keyLength: 40, expectedLabel: 'mock-addPassword.encryption.keyLength.40bit' },
+ { keyLength: 128, expectedLabel: 'mock-addPassword.encryption.keyLength.128bit' },
+ { keyLength: 256, expectedLabel: 'mock-addPassword.encryption.keyLength.256bit' }
+ ])('should handle key length $keyLength correctly', ({ keyLength, expectedLabel }) => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText(expectedLabel)).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/components/tools/addPassword/AddPasswordSettings.tsx b/frontend/src/components/tools/addPassword/AddPasswordSettings.tsx
new file mode 100644
index 000000000..6cce5ef0c
--- /dev/null
+++ b/frontend/src/components/tools/addPassword/AddPasswordSettings.tsx
@@ -0,0 +1,60 @@
+import React from "react";
+import { Stack, Text, PasswordInput, Select } from "@mantine/core";
+import { useTranslation } from "react-i18next";
+import { AddPasswordParameters } from "../../../hooks/tools/addPassword/useAddPasswordParameters";
+
+interface AddPasswordSettingsProps {
+ parameters: AddPasswordParameters;
+ onParameterChange: (key: keyof AddPasswordParameters, value: any) => void;
+ disabled?: boolean;
+}
+
+const AddPasswordSettings = ({ parameters, onParameterChange, disabled = false }: AddPasswordSettingsProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+ {/* Password Settings */}
+
+ {t('addPassword.passwords.title', 'Passwords')}
+ onParameterChange('password', e.target.value)}
+ disabled={disabled}
+ />
+ onParameterChange('ownerPassword', e.target.value)}
+ disabled={disabled}
+ />
+
+
+ {/* Encryption Settings */}
+
+ {t('addPassword.encryption.title', 'Encryption')}
+
+
+
+ );
+};
+
+export default AddPasswordSettings;
diff --git a/frontend/src/components/tools/changePermissions/ChangePermissionsSettings.test.tsx b/frontend/src/components/tools/changePermissions/ChangePermissionsSettings.test.tsx
new file mode 100644
index 000000000..51986266c
--- /dev/null
+++ b/frontend/src/components/tools/changePermissions/ChangePermissionsSettings.test.tsx
@@ -0,0 +1,226 @@
+import { describe, expect, test, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { MantineProvider } from '@mantine/core';
+import ChangePermissionsSettings from './ChangePermissionsSettings';
+import { defaultParameters } from '../../../hooks/tools/changePermissions/useChangePermissionsParameters';
+import type { ChangePermissionsParameters } from '../../../hooks/tools/changePermissions/useChangePermissionsParameters';
+
+// 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 }) => (
+ {children}
+);
+
+describe('ChangePermissionsSettings', () => {
+ const mockOnParameterChange = vi.fn();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ test('should render section title', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText('mock-changePermissions.restrictions.title')).toBeInTheDocument();
+ });
+
+ test('should render all permission checkboxes', () => {
+ render(
+
+
+
+ );
+
+ // Should render checkboxes for all permission types
+ const permissionKeys = Object.keys(defaultParameters) as Array;
+ const checkboxes = screen.getAllByRole('checkbox');
+ expect(checkboxes).toHaveLength(permissionKeys.length);
+
+ // Verify specific permission labels are rendered
+ expect(screen.getByText('mock-changePermissions.restrictions.preventAssembly.label')).toBeInTheDocument();
+ expect(screen.getByText('mock-changePermissions.restrictions.preventPrinting.label')).toBeInTheDocument();
+ expect(screen.getByText('mock-changePermissions.restrictions.preventModify.label')).toBeInTheDocument();
+ expect(screen.getByText('mock-changePermissions.restrictions.preventExtractContent.label')).toBeInTheDocument();
+ });
+
+ test('should render all permission types with correct labels', () => {
+ render(
+
+
+
+ );
+
+ const permissionKeys = Object.keys(defaultParameters) as Array;
+
+ permissionKeys.forEach(permission => {
+ expect(screen.getByText(`mock-changePermissions.restrictions.${permission}.label`)).toBeInTheDocument();
+ });
+ });
+
+ test('should show checkboxes as unchecked by default', () => {
+ render(
+
+
+
+ );
+
+ const checkboxes = screen.getAllByRole('checkbox') as HTMLInputElement[];
+ checkboxes.forEach(checkbox => {
+ expect(checkbox.checked).toBe(false);
+ });
+ });
+
+ test('should show checkboxes as checked when parameters are true', () => {
+ const checkedParameters: ChangePermissionsParameters = {
+ ...defaultParameters,
+ preventAssembly: true,
+ preventPrinting: true,
+ preventModify: true
+ };
+
+ render(
+
+
+
+ );
+
+ // Find specific checkboxes by their labels and verify they are checked
+ const assemblyCheckbox = screen.getByLabelText('mock-changePermissions.restrictions.preventAssembly.label') as HTMLInputElement;
+ const printingCheckbox = screen.getByLabelText('mock-changePermissions.restrictions.preventPrinting.label') as HTMLInputElement;
+ const modifyCheckbox = screen.getByLabelText('mock-changePermissions.restrictions.preventModify.label') as HTMLInputElement;
+ const formCheckbox = screen.getByLabelText('mock-changePermissions.restrictions.preventFillInForm.label') as HTMLInputElement;
+
+ expect(assemblyCheckbox.checked).toBe(true);
+ expect(printingCheckbox.checked).toBe(true);
+ expect(modifyCheckbox.checked).toBe(true);
+ expect(formCheckbox.checked).toBe(false); // Ensure other checkboxes are unaffected
+ });
+
+ test.each([
+ { initialValue: false, expectedValue: true, description: 'checking an unchecked box' },
+ { initialValue: true, expectedValue: false, description: 'unchecking a checked box' }
+ ])('should call onParameterChange with $expectedValue when $description', ({ initialValue, expectedValue }) => {
+ const testParameters: ChangePermissionsParameters = {
+ ...defaultParameters,
+ preventAssembly: initialValue
+ };
+
+ render(
+
+
+
+ );
+
+ const assemblyCheckbox = screen.getByLabelText('mock-changePermissions.restrictions.preventAssembly.label');
+ fireEvent.click(assemblyCheckbox);
+
+ expect(mockOnParameterChange).toHaveBeenCalledWith('preventAssembly', expectedValue);
+ });
+
+ test('should handle multiple checkbox interactions', () => {
+ render(
+
+
+
+ );
+
+ // Click multiple checkboxes
+ const assemblyCheckbox = screen.getByLabelText('mock-changePermissions.restrictions.preventAssembly.label');
+ const printingCheckbox = screen.getByLabelText('mock-changePermissions.restrictions.preventPrinting.label');
+
+ fireEvent.click(assemblyCheckbox);
+ fireEvent.click(printingCheckbox);
+
+ expect(mockOnParameterChange).toHaveBeenCalledWith('preventAssembly', true);
+ expect(mockOnParameterChange).toHaveBeenCalledWith('preventPrinting', true);
+ expect(mockOnParameterChange).toHaveBeenCalledTimes(2);
+ });
+
+ test.each([
+ { disabled: true, expectedState: true },
+ { disabled: false, expectedState: false }
+ ])('should set checkboxes disabled=$disabled when disabled prop is $disabled', ({ disabled, expectedState }) => {
+ render(
+
+
+
+ );
+
+ const checkboxes = screen.getAllByRole('checkbox') as HTMLInputElement[];
+ checkboxes.forEach(checkbox => {
+ expect(checkbox.disabled).toBe(expectedState);
+ });
+ });
+
+ test('should call translation function with correct keys', () => {
+ render(
+
+
+
+ );
+
+ // Verify that translation keys are being called
+ expect(mockT).toHaveBeenCalledWith('changePermissions.restrictions.title', 'Document Restrictions');
+
+ const permissionKeys = Object.keys(defaultParameters) as Array;
+ permissionKeys.forEach(permission => {
+ expect(mockT).toHaveBeenCalledWith(`changePermissions.restrictions.${permission}.label`, permission);
+ });
+ });
+
+ test.each(Object.keys(defaultParameters) as Array)('should handle %s permission type individually', (permission) => {
+ const testParameters: ChangePermissionsParameters = {
+ ...defaultParameters,
+ [permission]: true
+ };
+
+ render(
+
+
+
+ );
+
+ const checkbox = screen.getByLabelText(`mock-changePermissions.restrictions.${permission}.label`) as HTMLInputElement;
+ expect(checkbox.checked).toBe(true);
+ });
+});
diff --git a/frontend/src/components/tools/changePermissions/ChangePermissionsSettings.tsx b/frontend/src/components/tools/changePermissions/ChangePermissionsSettings.tsx
new file mode 100644
index 000000000..5f3c2d6a8
--- /dev/null
+++ b/frontend/src/components/tools/changePermissions/ChangePermissionsSettings.tsx
@@ -0,0 +1,33 @@
+import { Stack, Text, Checkbox } from "@mantine/core";
+import { useTranslation } from "react-i18next";
+import { ChangePermissionsParameters } from "../../../hooks/tools/changePermissions/useChangePermissionsParameters";
+
+interface ChangePermissionsSettingsProps {
+ parameters: ChangePermissionsParameters;
+ onParameterChange: (key: keyof ChangePermissionsParameters, value: boolean) => void;
+ disabled?: boolean;
+}
+
+const ChangePermissionsSettings = ({ parameters, onParameterChange, disabled = false }: ChangePermissionsSettingsProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+ {t('changePermissions.restrictions.title', 'Document Restrictions')}
+
+
+ {(Object.keys(parameters) as Array).map((key) => (
+ onParameterChange(key, e.target.checked)}
+ disabled={disabled}
+ />
+ ))}
+
+
+ );
+};
+
+export default ChangePermissionsSettings;
diff --git a/frontend/src/components/tools/split/SplitSettings.tsx b/frontend/src/components/tools/split/SplitSettings.tsx
index 632000227..2cf988f6d 100644
--- a/frontend/src/components/tools/split/SplitSettings.tsx
+++ b/frontend/src/components/tools/split/SplitSettings.tsx
@@ -1,19 +1,7 @@
import { Stack, TextInput, Select, Checkbox } from '@mantine/core';
import { useTranslation } from 'react-i18next';
-import { isSplitMode, SPLIT_MODES, SPLIT_TYPES, type SplitMode, type SplitType } from '../../../constants/splitConstants';
-
-export interface SplitParameters {
- mode: SplitMode | '';
- pages: string;
- hDiv: string;
- vDiv: string;
- merge: boolean;
- splitType: SplitType | '';
- splitValue: string;
- bookmarkLevel: string;
- includeMetadata: boolean;
- allowDuplicates: boolean;
-}
+import { isSplitMode, SPLIT_MODES, SPLIT_TYPES } from '../../../constants/splitConstants';
+import { SplitParameters } from '../../../hooks/tools/split/useSplitParameters';
export interface SplitSettingsProps {
parameters: SplitParameters;
diff --git a/frontend/src/components/tooltips/useAddPasswordTips.ts b/frontend/src/components/tooltips/useAddPasswordTips.ts
new file mode 100644
index 000000000..c47b8d0ee
--- /dev/null
+++ b/frontend/src/components/tooltips/useAddPasswordTips.ts
@@ -0,0 +1,35 @@
+import { useTranslation } from 'react-i18next';
+import { TooltipContent } from '../../types/tips';
+
+export const useAddPasswordTips = (): TooltipContent => {
+ const { t } = useTranslation();
+
+ return {
+ header: {
+ title: t("addPassword.tooltip.header.title", "Password Protection Overview")
+ },
+ tips: [
+ {
+ title: t("addPassword.tooltip.passwords.title", "Password Types"),
+ description: t("addPassword.tooltip.passwords.text", "User passwords restrict opening the document, while owner passwords control what can be done with the document once opened. You can set both or just one."),
+ bullets: [
+ t("addPassword.tooltip.passwords.bullet1", "User Password: Required to open the PDF"),
+ t("addPassword.tooltip.passwords.bullet2", "Owner Password: Controls document permissions (not supported by all PDF viewers)")
+ ]
+ },
+ {
+ title: t("addPassword.tooltip.encryption.title", "Encryption Levels"),
+ description: t("addPassword.tooltip.encryption.text", "Higher encryption levels provide better security but may not be supported by older PDF viewers."),
+ bullets: [
+ t("addPassword.tooltip.encryption.bullet1", "40-bit: Basic security, compatible with older viewers"),
+ t("addPassword.tooltip.encryption.bullet2", "128-bit: Standard security, widely supported"),
+ t("addPassword.tooltip.encryption.bullet3", "256-bit: Maximum security, requires modern viewers")
+ ]
+ },
+ {
+ title: t("addPassword.tooltip.restrictions.title", "Document Restrictions"),
+ description: t("addPassword.tooltip.restrictions.text", "These restrictions control what users can do with the PDF. Most effective when combined with an owner password."),
+ }
+ ]
+ };
+};
diff --git a/frontend/src/components/tooltips/useChangePermissionsTips.ts b/frontend/src/components/tooltips/useChangePermissionsTips.ts
new file mode 100644
index 000000000..533a21b84
--- /dev/null
+++ b/frontend/src/components/tooltips/useChangePermissionsTips.ts
@@ -0,0 +1,18 @@
+import { useTranslation } from 'react-i18next';
+import { TooltipContent } from '../../types/tips';
+
+export const useChangePermissionsTips = (): TooltipContent => {
+ const { t } = useTranslation();
+
+ return {
+ header: {
+ title: t("changePermissions.tooltip.header.title", "Change Permissions")
+ },
+ tips: [
+ {
+ title: t("changePermissions.tooltip.description.title", "Description"),
+ description: t("changePermissions.tooltip.description.text", "Changes document permissions. Warning: To make these restrictions unchangeable, use the Add Password tool to set an owner password.")
+ }
+ ]
+ };
+};
diff --git a/frontend/src/components/tooltips/CompressTips.ts b/frontend/src/components/tooltips/useCompressTips.ts
similarity index 96%
rename from frontend/src/components/tooltips/CompressTips.ts
rename to frontend/src/components/tooltips/useCompressTips.ts
index 2fb2a0777..c42e8d63a 100644
--- a/frontend/src/components/tooltips/CompressTips.ts
+++ b/frontend/src/components/tooltips/useCompressTips.ts
@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '../../types/tips';
-export const CompressTips = (): TooltipContent => {
+export const useCompressTips = (): TooltipContent => {
const { t } = useTranslation();
return {
@@ -27,4 +27,4 @@ export const CompressTips = (): TooltipContent => {
}
]
};
-};
\ No newline at end of file
+};
diff --git a/frontend/src/components/tooltips/OCRTips.ts b/frontend/src/components/tooltips/useOCRTips.ts
similarity index 96%
rename from frontend/src/components/tooltips/OCRTips.ts
rename to frontend/src/components/tooltips/useOCRTips.ts
index 1002182f2..0ae5e387e 100644
--- a/frontend/src/components/tooltips/OCRTips.ts
+++ b/frontend/src/components/tooltips/useOCRTips.ts
@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '../../types/tips';
-export const OcrTips = (): TooltipContent => {
+export const useOCRTips = (): TooltipContent => {
const { t } = useTranslation();
return {
@@ -33,4 +33,4 @@ export const OcrTips = (): TooltipContent => {
}
]
};
-};
\ No newline at end of file
+};
diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx
index 953c7720f..259f92d09 100644
--- a/frontend/src/contexts/FileContext.tsx
+++ b/frontend/src/contexts/FileContext.tsx
@@ -460,9 +460,7 @@ export function FileContextProvider({
thumbnailGenerationService.destroy();
// Force garbage collection hint
- if (typeof window !== 'undefined') {
- setTimeout(() => window.gc && window.gc(), 100);
- }
+ setTimeout(() => window?.gc?.(), 100);
} catch (error) {
console.warn('Error during cleanup all files:', error);
@@ -596,9 +594,9 @@ export function FileContextProvider({
dispatch({ type: 'SET_CURRENT_MODE', payload: mode });
if (state.currentMode !== mode && state.activeFiles.length > 0) {
- if (window.requestIdleCallback && typeof window !== 'undefined') {
+ if (window.requestIdleCallback) {
window.requestIdleCallback(() => {
- window.gc && window.gc();
+ window.gc?.();
}, { timeout: 5000 });
}
}
@@ -610,9 +608,9 @@ export function FileContextProvider({
dispatch({ type: 'SET_CURRENT_VIEW', payload: view });
if (state.currentView !== view && state.activeFiles.length > 0) {
- if (window.requestIdleCallback && typeof window !== 'undefined') {
+ if (window.requestIdleCallback) {
window.requestIdleCallback(() => {
- window.gc && window.gc();
+ window.gc?.();
}, { timeout: 5000 });
}
}
diff --git a/frontend/src/hooks/tools/addPassword/useAddPasswordOperation.test.ts b/frontend/src/hooks/tools/addPassword/useAddPasswordOperation.test.ts
new file mode 100644
index 000000000..5382f2f99
--- /dev/null
+++ b/frontend/src/hooks/tools/addPassword/useAddPasswordOperation.test.ts
@@ -0,0 +1,144 @@
+import { describe, expect, test, vi, beforeEach, MockedFunction } from 'vitest';
+import { renderHook } from '@testing-library/react';
+import { useAddPasswordOperation } from './useAddPasswordOperation';
+import type { AddPasswordFullParameters, AddPasswordParameters } from './useAddPasswordParameters';
+
+// Mock the useToolOperation hook
+vi.mock('../shared/useToolOperation', () => ({
+ useToolOperation: vi.fn()
+}));
+
+// Mock the translation hook
+const mockT = vi.fn((key: string) => `translated-${key}`);
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({ t: mockT })
+}));
+
+// Mock the error handler
+vi.mock('../../../utils/toolErrorHandler', () => ({
+ createStandardErrorHandler: vi.fn(() => 'error-handler-function')
+}));
+
+// Import the mocked function
+import { ToolOperationConfig, ToolOperationHook, useToolOperation } from '../shared/useToolOperation';
+
+
+describe('useAddPasswordOperation', () => {
+ const mockUseToolOperation = vi.mocked(useToolOperation);
+
+ const getToolConfig = (): ToolOperationConfig => mockUseToolOperation.mock.calls[0][0];
+
+ const mockToolOperationReturn: ToolOperationHook = {
+ files: [],
+ thumbnails: [],
+ downloadUrl: null,
+ downloadFilename: '',
+ isLoading: false,
+ errorMessage: null,
+ status: '',
+ isGeneratingThumbnails: false,
+ progress: null,
+ executeOperation: vi.fn(),
+ resetResults: vi.fn(),
+ clearError: vi.fn(),
+ cancelOperation: vi.fn(),
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockUseToolOperation.mockReturnValue(mockToolOperationReturn);
+ });
+
+ test('should configure useToolOperation with correct parameters', () => {
+ renderHook(() => useAddPasswordOperation());
+
+ expect(mockUseToolOperation).toHaveBeenCalledWith({
+ operationType: 'addPassword',
+ endpoint: '/api/v1/security/add-password',
+ buildFormData: expect.any(Function),
+ filePrefix: 'translated-addPassword.filenamePrefix_',
+ multiFileEndpoint: false,
+ getErrorMessage: 'error-handler-function'
+ });
+ });
+
+ test('should return the result from useToolOperation', () => {
+ const { result } = renderHook(() => useAddPasswordOperation());
+
+ expect(result.current).toBe(mockToolOperationReturn);
+ });
+
+ test.each([
+ {
+ description: 'with all parameters filled',
+ password: 'user-password',
+ ownerPassword: 'owner-password',
+ keyLength: 256
+ },
+ {
+ description: 'with empty passwords',
+ password: '',
+ ownerPassword: '',
+ keyLength: 128
+ },
+ {
+ description: 'with 40-bit key length',
+ password: 'test',
+ ownerPassword: '',
+ keyLength: 40
+ }
+ ])('should create form data correctly $description', ({ password, ownerPassword, keyLength }) => {
+ renderHook(() => useAddPasswordOperation());
+
+ const callArgs = getToolConfig();
+ const buildFormData = callArgs.buildFormData;
+
+ const testParameters: AddPasswordFullParameters = {
+ password,
+ ownerPassword,
+ keyLength,
+ permissions: {
+ preventAssembly: false,
+ preventExtractContent: false,
+ preventExtractForAccessibility: false,
+ preventFillInForm: false,
+ preventModify: false,
+ preventModifyAnnotations: false,
+ preventPrinting: false,
+ preventPrintingFaithful: false
+ }
+ };
+
+ const testFile = new File(['test content'], 'test.pdf', { type: 'application/pdf' });
+ const formData = buildFormData(testParameters, testFile as any /* FIX ME */);
+
+ // Verify the form data contains the file
+ expect(formData.get('fileInput')).toBe(testFile);
+
+ // Verify password parameters
+ expect(formData.get('password')).toBe(password);
+ expect(formData.get('ownerPassword')).toBe(ownerPassword);
+ expect(formData.get('keyLength')).toBe(keyLength.toString());
+ });
+
+ test('should use correct translation for error messages', () => {
+ renderHook(() => useAddPasswordOperation());
+
+ expect(mockT).toHaveBeenCalledWith(
+ 'addPassword.error.failed',
+ 'An error occurred while encrypting the PDF.'
+ );
+ });
+
+ test.each([
+ { property: 'multiFileEndpoint' as const, expectedValue: false },
+ { property: 'endpoint' as const, expectedValue: '/api/v1/security/add-password' },
+ { property: 'filePrefix' as const, expectedValue: 'translated-addPassword.filenamePrefix_' },
+ { property: 'operationType' as const, expectedValue: 'addPassword' }
+ ])('should configure $property correctly', ({ property, expectedValue }) => {
+ renderHook(() => useAddPasswordOperation());
+
+ const callArgs = getToolConfig();
+ expect(callArgs[property]).toBe(expectedValue);
+ });
+});
diff --git a/frontend/src/hooks/tools/addPassword/useAddPasswordOperation.ts b/frontend/src/hooks/tools/addPassword/useAddPasswordOperation.ts
new file mode 100644
index 000000000..d94b9650e
--- /dev/null
+++ b/frontend/src/hooks/tools/addPassword/useAddPasswordOperation.ts
@@ -0,0 +1,30 @@
+import { useTranslation } from 'react-i18next';
+import { useToolOperation } from '../shared/useToolOperation';
+import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
+import { AddPasswordFullParameters } from './useAddPasswordParameters';
+import { getFormData } from '../changePermissions/useChangePermissionsOperation';
+
+export const useAddPasswordOperation = () => {
+ const { t } = useTranslation();
+
+ const buildFormData = (parameters: AddPasswordFullParameters, file: File): FormData => {
+ const formData = new FormData();
+ formData.append("fileInput", file);
+ formData.append("password", parameters.password);
+ formData.append("ownerPassword", parameters.ownerPassword);
+ formData.append("keyLength", parameters.keyLength.toString());
+ getFormData(parameters.permissions).forEach(([key, value]) => {
+ formData.append(key, value);
+ });
+ return formData;
+ };
+
+ return useToolOperation({
+ operationType: 'addPassword',
+ endpoint: '/api/v1/security/add-password',
+ buildFormData,
+ filePrefix: t('addPassword.filenamePrefix', 'encrypted') + '_',
+ multiFileEndpoint: false,
+ getErrorMessage: createStandardErrorHandler(t('addPassword.error.failed', 'An error occurred while encrypting the PDF.'))
+ });
+};
diff --git a/frontend/src/hooks/tools/addPassword/useAddPasswordParameters.test.ts b/frontend/src/hooks/tools/addPassword/useAddPasswordParameters.test.ts
new file mode 100644
index 000000000..92a876087
--- /dev/null
+++ b/frontend/src/hooks/tools/addPassword/useAddPasswordParameters.test.ts
@@ -0,0 +1,152 @@
+import { describe, expect, test } from 'vitest';
+import { renderHook, act } from '@testing-library/react';
+import { useAddPasswordParameters, defaultParameters, AddPasswordParametersHook } from './useAddPasswordParameters';
+import { defaultParameters as defaultChangePermissionsParameters, ChangePermissionsParameters } from '../changePermissions/useChangePermissionsParameters';
+
+describe('useAddPasswordParameters', () => {
+ test('should initialize with default parameters', () => {
+ const { result } = renderHook(() => useAddPasswordParameters());
+
+ expect(result.current.parameters).toStrictEqual(defaultParameters);
+ });
+
+ test.each([
+ { paramName: 'password' as const, value: 'test-password' },
+ { paramName: 'ownerPassword' as const, value: 'owner-password' },
+ { paramName: 'keyLength' as const, value: 256 }
+ ])('should update parameter $paramName', ({ paramName, value }) => {
+ const { result } = renderHook(() => useAddPasswordParameters());
+
+ act(() => {
+ result.current.updateParameter(paramName, value);
+ });
+
+ expect(result.current.parameters[paramName]).toBe(value);
+ });
+
+ test.each([
+ { paramName: 'preventAssembly' as const },
+ { paramName: 'preventPrinting' as const }
+ ])('should update boolean permission parameter $paramName', ({ paramName }) => {
+ const { result } = renderHook(() => useAddPasswordParameters());
+
+ act(() => {
+ result.current.permissions.updateParameter(paramName, true);
+ });
+
+ expect(result.current.permissions.parameters[paramName]).toBe(true);
+ });
+
+ test('should reset parameters to defaults', () => {
+ const { result } = renderHook(() => useAddPasswordParameters());
+
+ // First, change some parameters
+ act(() => {
+ result.current.updateParameter('password', 'test');
+ result.current.updateParameter('keyLength', 256);
+ result.current.permissions.updateParameter('preventAssembly', true);
+ });
+
+ expect(result.current.parameters.password).toBe('test');
+ expect(result.current.parameters.keyLength).toBe(256);
+ expect(result.current.permissions.parameters.preventAssembly).toBe(true);
+
+ // Then reset
+ act(() => {
+ result.current.resetParameters();
+ });
+
+ expect(result.current.parameters).toStrictEqual(defaultParameters);
+ });
+
+ test('should return correct endpoint name', () => {
+ const { result } = renderHook(() => useAddPasswordParameters());
+
+ expect(result.current.getEndpointName()).toBe('add-password');
+ });
+
+ test.each([
+ {
+ description: 'with user password only',
+ setup: (hook: AddPasswordParametersHook) => {
+ hook.updateParameter('password', 'user-password');
+ }
+ },
+ {
+ description: 'with owner password only',
+ setup: (hook: AddPasswordParametersHook) => {
+ hook.updateParameter('ownerPassword', 'owner-password');
+ }
+ },
+ {
+ description: 'with both passwords',
+ setup: (hook: AddPasswordParametersHook) => {
+ hook.updateParameter('password', 'user-password');
+ hook.updateParameter('ownerPassword', 'owner-password');
+ }
+ },
+ {
+ description: 'with whitespace only password',
+ setup: (hook: AddPasswordParametersHook) => {
+ hook.updateParameter('password', ' \t ');
+ }
+ },
+ {
+ description: 'with whitespace only owner password',
+ setup: (hook: AddPasswordParametersHook) => {
+ hook.updateParameter('ownerPassword', ' \t ');
+ }
+ },
+ {
+ description: 'with restrictions only',
+ setup: (hook: AddPasswordParametersHook) => {
+ hook.permissions.updateParameter('preventAssembly', true);
+ hook.permissions.updateParameter('preventPrinting', true);
+ }
+ },
+ {
+ description: 'with passwords and restrictions',
+ setup: (hook: AddPasswordParametersHook) => {
+ hook.updateParameter('password', 'test-password');
+ hook.permissions.updateParameter('preventAssembly', true);
+ }
+ }
+ ])('should validate parameters correctly $description', ({ setup }) => {
+ const { result } = renderHook(() => useAddPasswordParameters());
+
+ // Default state should be valid
+ expect(result.current.validateParameters()).toBe(true);
+
+ // Apply the test scenario setup
+ act(() => {
+ setup(result.current);
+ });
+
+ expect(result.current.validateParameters()).toBe(true);
+ });
+
+ test.each(Object.keys(defaultChangePermissionsParameters) as Array)('should handle boolean restriction parameter %s', (param) => {
+ const { result } = renderHook(() => useAddPasswordParameters());
+
+ act(() => {
+ result.current.resetParameters();
+ result.current.permissions.updateParameter(param, true);
+ });
+
+ expect(result.current.validateParameters()).toBe(true);
+ });
+
+ test('should handle mixed parameter types in updateParameter', () => {
+ const { result } = renderHook(() => useAddPasswordParameters());
+
+ act(() => {
+ result.current.updateParameter('password', 'test-string');
+ result.current.updateParameter('keyLength', 40);
+ result.current.permissions.updateParameter('preventAssembly', true);
+ });
+
+ expect(result.current.parameters.password).toBe('test-string');
+ expect(result.current.parameters.keyLength).toBe(40);
+ expect(result.current.permissions.parameters.preventAssembly).toBe(true);
+ });
+});
diff --git a/frontend/src/hooks/tools/addPassword/useAddPasswordParameters.ts b/frontend/src/hooks/tools/addPassword/useAddPasswordParameters.ts
new file mode 100644
index 000000000..ad428ebd5
--- /dev/null
+++ b/frontend/src/hooks/tools/addPassword/useAddPasswordParameters.ts
@@ -0,0 +1,69 @@
+import { useState } from 'react';
+import { ChangePermissionsParameters, ChangePermissionsParametersHook, useChangePermissionsParameters } from '../changePermissions/useChangePermissionsParameters';
+
+export interface AddPasswordParameters {
+ password: string;
+ ownerPassword: string;
+ keyLength: number;
+}
+
+export interface AddPasswordFullParameters extends AddPasswordParameters {
+ permissions: ChangePermissionsParameters;
+}
+
+export interface AddPasswordParametersHook {
+ fullParameters: AddPasswordFullParameters;
+ parameters: AddPasswordParameters;
+ permissions: ChangePermissionsParametersHook;
+ updateParameter: (parameter: K, value: AddPasswordParameters[K]) => void;
+ resetParameters: () => void;
+ validateParameters: () => boolean;
+ getEndpointName: () => string;
+}
+
+export const defaultParameters: AddPasswordParameters = {
+ password: '',
+ ownerPassword: '',
+ keyLength: 128,
+};
+
+export const useAddPasswordParameters = (): AddPasswordParametersHook => {
+ const [parameters, setParameters] = useState(defaultParameters);
+ const permissions = useChangePermissionsParameters();
+ const fullParameters: AddPasswordFullParameters = {
+ ...parameters,
+ permissions: permissions.parameters,
+ };
+
+ const updateParameter = (parameter: K, value: AddPasswordParameters[K]) => {
+ setParameters(prev => ({
+ ...prev,
+ [parameter]: value,
+ })
+ );
+ };
+
+ const resetParameters = () => {
+ setParameters(defaultParameters);
+ permissions.resetParameters();
+ };
+
+ const validateParameters = () => {
+ // No required parameters for Add Password. Defer to permissions validation.
+ return permissions.validateParameters();
+ };
+
+ const getEndpointName = () => {
+ return 'add-password';
+ };
+
+ return {
+ fullParameters,
+ parameters,
+ permissions,
+ updateParameter,
+ resetParameters,
+ validateParameters,
+ getEndpointName,
+ };
+};
diff --git a/frontend/src/hooks/tools/changePermissions/useChangePermissionsOperation.test.ts b/frontend/src/hooks/tools/changePermissions/useChangePermissionsOperation.test.ts
new file mode 100644
index 000000000..f6cedc0c3
--- /dev/null
+++ b/frontend/src/hooks/tools/changePermissions/useChangePermissionsOperation.test.ts
@@ -0,0 +1,138 @@
+import { describe, expect, test, vi, beforeEach } from 'vitest';
+import { renderHook } from '@testing-library/react';
+import { useChangePermissionsOperation } from './useChangePermissionsOperation';
+import type { ChangePermissionsParameters } from './useChangePermissionsParameters';
+
+// Mock the useToolOperation hook
+vi.mock('../shared/useToolOperation', () => ({
+ useToolOperation: vi.fn()
+}));
+
+// Mock the translation hook
+const mockT = vi.fn((key: string) => `translated-${key}`);
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({ t: mockT })
+}));
+
+// Mock the error handler
+vi.mock('../../../utils/toolErrorHandler', () => ({
+ createStandardErrorHandler: vi.fn(() => 'error-handler-function')
+}));
+
+// Import the mocked function
+import { ToolOperationConfig, ToolOperationHook, useToolOperation } from '../shared/useToolOperation';
+
+describe('useChangePermissionsOperation', () => {
+ const mockUseToolOperation = vi.mocked(useToolOperation);
+
+ const getToolConfig = (): ToolOperationConfig => mockUseToolOperation.mock.calls[0][0];
+
+ const mockToolOperationReturn: ToolOperationHook = {
+ files: [],
+ thumbnails: [],
+ downloadUrl: null,
+ downloadFilename: '',
+ isLoading: false,
+ errorMessage: null,
+ status: '',
+ isGeneratingThumbnails: false,
+ progress: null,
+ executeOperation: vi.fn(),
+ resetResults: vi.fn(),
+ clearError: vi.fn(),
+ cancelOperation: vi.fn(),
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockUseToolOperation.mockReturnValue(mockToolOperationReturn);
+ });
+
+ test('should configure useToolOperation with correct parameters', () => {
+ renderHook(() => useChangePermissionsOperation());
+
+ expect(mockUseToolOperation).toHaveBeenCalledWith({
+ operationType: 'changePermissions',
+ endpoint: '/api/v1/security/add-password',
+ buildFormData: expect.any(Function),
+ filePrefix: 'permissions_',
+ multiFileEndpoint: false,
+ getErrorMessage: 'error-handler-function'
+ });
+ });
+
+ test('should return the result from useToolOperation', () => {
+ const { result } = renderHook(() => useChangePermissionsOperation());
+
+ expect(result.current).toBe(mockToolOperationReturn);
+ });
+
+ test.each([
+ {
+ preventAssembly: false,
+ preventExtractContent: false,
+ preventExtractForAccessibility: false,
+ preventFillInForm: false,
+ preventModify: false,
+ preventModifyAnnotations: false,
+ preventPrinting: false,
+ preventPrintingFaithful: false,
+ },
+ {
+ preventAssembly: true,
+ preventExtractContent: false,
+ preventExtractForAccessibility: true,
+ preventFillInForm: false,
+ preventModify: true,
+ preventModifyAnnotations: false,
+ preventPrinting: true,
+ preventPrintingFaithful: false,
+ },
+ {
+ preventAssembly: true,
+ preventExtractContent: true,
+ preventExtractForAccessibility: true,
+ preventFillInForm: true,
+ preventModify: true,
+ preventModifyAnnotations: true,
+ preventPrinting: true,
+ preventPrintingFaithful: true,
+ },
+ ])('should create form data correctly', (testParameters: ChangePermissionsParameters) => {
+ renderHook(() => useChangePermissionsOperation());
+
+ const callArgs = getToolConfig();
+ const buildFormData = callArgs.buildFormData;
+
+ const testFile = new File(['test content'], 'test.pdf', { type: 'application/pdf' });
+ const formData = buildFormData(testParameters, testFile as any /* FIX ME */);
+
+ // Verify the form data contains the file
+ expect(formData.get('fileInput')).toBe(testFile);
+
+ (Object.keys(testParameters) as Array).forEach(key => {
+ expect(formData.get(key), `Parameter ${key} should be set correctly`).toBe(testParameters[key].toString());
+ });
+ });
+
+ test('should use correct translation for error messages', () => {
+ renderHook(() => useChangePermissionsOperation());
+
+ expect(mockT).toHaveBeenCalledWith(
+ 'changePermissions.error.failed',
+ 'An error occurred while changing PDF permissions.'
+ );
+ });
+
+ test.each([
+ { property: 'multiFileEndpoint' as const, expectedValue: false },
+ { property: 'endpoint' as const, expectedValue: '/api/v1/security/add-password' },
+ { property: 'filePrefix' as const, expectedValue: 'permissions_' },
+ { property: 'operationType' as const, expectedValue: 'changePermissions' }
+ ])('should configure $property correctly', ({ property, expectedValue }) => {
+ renderHook(() => useChangePermissionsOperation());
+
+ const callArgs = getToolConfig();
+ expect(callArgs[property]).toBe(expectedValue);
+ });
+});
diff --git a/frontend/src/hooks/tools/changePermissions/useChangePermissionsOperation.ts b/frontend/src/hooks/tools/changePermissions/useChangePermissionsOperation.ts
new file mode 100644
index 000000000..664a423ff
--- /dev/null
+++ b/frontend/src/hooks/tools/changePermissions/useChangePermissionsOperation.ts
@@ -0,0 +1,37 @@
+import { useTranslation } from 'react-i18next';
+import { useToolOperation } from '../shared/useToolOperation';
+import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
+import type { ChangePermissionsParameters } from './useChangePermissionsParameters';
+
+export const getFormData = ((parameters: ChangePermissionsParameters) =>
+ Object.entries(parameters).map(([key, value]) =>
+ [key, value.toString()]
+ ) as string[][]
+);
+
+export const useChangePermissionsOperation = () => {
+ const { t } = useTranslation();
+
+ const buildFormData = (parameters: ChangePermissionsParameters, file: File): FormData => {
+ const formData = new FormData();
+ formData.append("fileInput", file);
+
+ // Add all permission parameters
+ getFormData(parameters).forEach(([key, value]) => {
+ formData.append(key, value);
+ });
+
+ return formData;
+ };
+
+ return useToolOperation({
+ operationType: 'changePermissions',
+ endpoint: '/api/v1/security/add-password', // Change Permissions is a fake endpoint for the Add Password tool
+ buildFormData,
+ filePrefix: 'permissions_',
+ multiFileEndpoint: false,
+ getErrorMessage: createStandardErrorHandler(
+ t('changePermissions.error.failed', 'An error occurred while changing PDF permissions.')
+ )
+ });
+};
diff --git a/frontend/src/hooks/tools/changePermissions/useChangePermissionsParameters.test.ts b/frontend/src/hooks/tools/changePermissions/useChangePermissionsParameters.test.ts
new file mode 100644
index 000000000..fe85434a5
--- /dev/null
+++ b/frontend/src/hooks/tools/changePermissions/useChangePermissionsParameters.test.ts
@@ -0,0 +1,110 @@
+import { describe, expect, test } from 'vitest';
+import { renderHook, act } from '@testing-library/react';
+import { useChangePermissionsParameters, defaultParameters, ChangePermissionsParameters } from './useChangePermissionsParameters';
+
+describe('useChangePermissionsParameters', () => {
+ test('should initialize with default parameters', () => {
+ const { result } = renderHook(() => useChangePermissionsParameters());
+
+ expect(result.current.parameters).toStrictEqual(defaultParameters);
+ });
+
+ test('should update individual boolean parameters', () => {
+ const { result } = renderHook(() => useChangePermissionsParameters());
+
+ act(() => {
+ result.current.updateParameter('preventAssembly', true);
+ });
+
+ expect(result.current.parameters.preventAssembly).toBe(true);
+ expect(result.current.parameters.preventPrinting).toBe(false); // Other parameters should remain unchanged
+
+ act(() => {
+ result.current.updateParameter('preventPrinting', true);
+ });
+
+ expect(result.current.parameters.preventPrinting).toBe(true);
+ expect(result.current.parameters.preventAssembly).toBe(true);
+ });
+
+ test('should update all permission parameters', () => {
+ const { result } = renderHook(() => useChangePermissionsParameters());
+
+ const permissionKeys = Object.keys(defaultParameters) as Array;
+
+ // Set all to true
+ act(() => {
+ permissionKeys.forEach(key => {
+ result.current.updateParameter(key, true);
+ });
+ });
+
+ permissionKeys.forEach(key => {
+ expect(result.current.parameters[key]).toBe(true);
+ });
+
+ // Set all to false
+ act(() => {
+ permissionKeys.forEach(key => {
+ result.current.updateParameter(key, false);
+ });
+ });
+
+ permissionKeys.forEach(key => {
+ expect(result.current.parameters[key]).toBe(false);
+ });
+ });
+
+ test('should reset parameters to defaults', () => {
+ const { result } = renderHook(() => useChangePermissionsParameters());
+
+ // First, change some parameters
+ act(() => {
+ result.current.updateParameter('preventAssembly', true);
+ result.current.updateParameter('preventPrinting', true);
+ result.current.updateParameter('preventModify', true);
+ });
+
+ expect(result.current.parameters.preventAssembly).toBe(true);
+ expect(result.current.parameters.preventPrinting).toBe(true);
+ expect(result.current.parameters.preventModify).toBe(true);
+
+ // Then reset
+ act(() => {
+ result.current.resetParameters();
+ });
+
+ expect(result.current.parameters).toStrictEqual(defaultParameters);
+ });
+
+ test('should return correct endpoint name', () => {
+ const { result } = renderHook(() => useChangePermissionsParameters());
+
+ expect(result.current.getEndpointName()).toBe('add-password');
+ });
+
+ test('should always validate as true', () => {
+ const { result } = renderHook(() => useChangePermissionsParameters());
+
+ // Default state should be valid
+ expect(result.current.validateParameters()).toBe(true);
+
+ // Set some restrictions - should still be valid
+ act(() => {
+ result.current.updateParameter('preventAssembly', true);
+ result.current.updateParameter('preventPrinting', true);
+ });
+
+ expect(result.current.validateParameters()).toBe(true);
+
+ // Set all restrictions - should still be valid
+ act(() => {
+ const permissionKeys = Object.keys(defaultParameters) as Array;
+ permissionKeys.forEach(key => {
+ result.current.updateParameter(key, true);
+ });
+ });
+
+ expect(result.current.validateParameters()).toBe(true);
+ });
+});
diff --git a/frontend/src/hooks/tools/changePermissions/useChangePermissionsParameters.ts b/frontend/src/hooks/tools/changePermissions/useChangePermissionsParameters.ts
new file mode 100644
index 000000000..43d21f36f
--- /dev/null
+++ b/frontend/src/hooks/tools/changePermissions/useChangePermissionsParameters.ts
@@ -0,0 +1,64 @@
+import { useState } from 'react';
+
+export interface ChangePermissionsParameters {
+ preventAssembly: boolean;
+ preventExtractContent: boolean;
+ preventExtractForAccessibility: boolean;
+ preventFillInForm: boolean;
+ preventModify: boolean;
+ preventModifyAnnotations: boolean;
+ preventPrinting: boolean;
+ preventPrintingFaithful: boolean;
+}
+
+export interface ChangePermissionsParametersHook {
+ parameters: ChangePermissionsParameters;
+ updateParameter: (parameter: keyof ChangePermissionsParameters, value: boolean) => void;
+ resetParameters: () => void;
+ validateParameters: () => boolean;
+ getEndpointName: () => string;
+}
+
+export const defaultParameters: ChangePermissionsParameters = {
+ preventAssembly: false,
+ preventExtractContent: false,
+ preventExtractForAccessibility: false,
+ preventFillInForm: false,
+ preventModify: false,
+ preventModifyAnnotations: false,
+ preventPrinting: false,
+ preventPrintingFaithful: false,
+};
+
+export const useChangePermissionsParameters = (): ChangePermissionsParametersHook => {
+ const [parameters, setParameters] = useState(defaultParameters);
+
+ const updateParameter = (parameter: K, value: ChangePermissionsParameters[K]) => {
+ setParameters(prev => ({
+ ...prev,
+ [parameter]: value,
+ })
+ );
+ };
+
+ const resetParameters = () => {
+ setParameters(defaultParameters);
+ };
+
+ const validateParameters = () => {
+ // Always valid - any combination of permissions is allowed
+ return true;
+ };
+
+ const getEndpointName = () => {
+ return 'add-password'; // Change Permissions is a fake endpoint for the Add Password tool
+ };
+
+ return {
+ parameters,
+ updateParameter,
+ resetParameters,
+ validateParameters,
+ getEndpointName,
+ };
+};
diff --git a/frontend/src/hooks/tools/shared/useToolOperation.ts b/frontend/src/hooks/tools/shared/useToolOperation.ts
index 9fa883490..d74575f51 100644
--- a/frontend/src/hooks/tools/shared/useToolOperation.ts
+++ b/frontend/src/hooks/tools/shared/useToolOperation.ts
@@ -36,7 +36,7 @@ export interface ToolOperationConfig {
* - (params, files: File[]) => FormData: Multi-file processing
* Not used when customProcessor is provided.
*/
- buildFormData: ((params: TParams, file: File) => FormData) | ((params: TParams, files: File[]) => FormData);
+ buildFormData: ((params: TParams, file: File) => FormData) | ((params: TParams, files: File[]) => FormData); /* FIX ME */
/** Prefix added to processed filenames (e.g., 'compressed_', 'split_') */
filePrefix: string;
diff --git a/frontend/src/hooks/tools/split/useSplitOperation.ts b/frontend/src/hooks/tools/split/useSplitOperation.ts
index 184289e18..7702bffad 100644
--- a/frontend/src/hooks/tools/split/useSplitOperation.ts
+++ b/frontend/src/hooks/tools/split/useSplitOperation.ts
@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
-import { SplitParameters } from '../../../components/tools/split/SplitSettings';
+import { SplitParameters } from './useSplitParameters';
import { SPLIT_MODES } from '../../../constants/splitConstants';
diff --git a/frontend/src/hooks/tools/split/useSplitParameters.ts b/frontend/src/hooks/tools/split/useSplitParameters.ts
index 54003e79c..dd3eb218f 100644
--- a/frontend/src/hooks/tools/split/useSplitParameters.ts
+++ b/frontend/src/hooks/tools/split/useSplitParameters.ts
@@ -1,6 +1,18 @@
import { useState } from 'react';
-import { SPLIT_MODES, SPLIT_TYPES, ENDPOINTS, type SplitMode } from '../../../constants/splitConstants';
-import { SplitParameters } from '../../../components/tools/split/SplitSettings';
+import { SPLIT_MODES, SPLIT_TYPES, ENDPOINTS, type SplitMode, SplitType } from '../../../constants/splitConstants';
+
+export interface SplitParameters {
+ mode: SplitMode | '';
+ pages: string;
+ hDiv: string;
+ vDiv: string;
+ merge: boolean;
+ splitType: SplitType | '';
+ splitValue: string;
+ bookmarkLevel: string;
+ includeMetadata: boolean;
+ allowDuplicates: boolean;
+}
export interface SplitParametersHook {
parameters: SplitParameters;
diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx
index 1de26814d..75b656e34 100644
--- a/frontend/src/hooks/useToolManagement.tsx
+++ b/frontend/src/hooks/useToolManagement.tsx
@@ -5,6 +5,7 @@ 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 LockIcon from "@mui/icons-material/Lock";
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool";
@@ -85,6 +86,24 @@ const toolDefinitions: Record = {
description: "Remove potentially harmful elements from PDF files",
endpoints: ["sanitize-pdf"]
},
+ addPassword: {
+ id: "addPassword",
+ icon: ,
+ component: React.lazy(() => import("../tools/AddPassword")),
+ maxFiles: -1,
+ category: "security",
+ description: "Add password protection and restrictions to PDF files",
+ endpoints: ["add-password"]
+ },
+ changePermissions: {
+ id: "changePermissions",
+ icon: ,
+ component: React.lazy(() => import("../tools/ChangePermissions")),
+ maxFiles: -1,
+ category: "security",
+ description: "Change document restrictions and permissions",
+ endpoints: ["add-password"]
+ },
};
diff --git a/frontend/src/services/enhancedPDFProcessingService.ts b/frontend/src/services/enhancedPDFProcessingService.ts
index 64bf69b8a..086846515 100644
--- a/frontend/src/services/enhancedPDFProcessingService.ts
+++ b/frontend/src/services/enhancedPDFProcessingService.ts
@@ -519,9 +519,7 @@ export class EnhancedPDFProcessingService {
this.notifyListeners();
// Force memory cleanup hint
- if (typeof window !== 'undefined') {
- setTimeout(() => window.gc && window.gc(), 100);
- }
+ setTimeout(() => window?.gc?.(), 100);
}
/**
diff --git a/frontend/src/tools/AddPassword.tsx b/frontend/src/tools/AddPassword.tsx
new file mode 100644
index 000000000..773fb4d1c
--- /dev/null
+++ b/frontend/src/tools/AddPassword.tsx
@@ -0,0 +1,189 @@
+import { useEffect, useMemo, useState } from "react";
+import { Box, Button, Stack, Text } from "@mantine/core";
+import { useTranslation } from "react-i18next";
+import DownloadIcon from "@mui/icons-material/Download";
+import { useEndpointEnabled } from "../hooks/useEndpointConfig";
+import { useFileContext } from "../contexts/FileContext";
+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 AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
+import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
+
+import { useAddPasswordParameters } from "../hooks/tools/addPassword/useAddPasswordParameters";
+import { useAddPasswordOperation } from "../hooks/tools/addPassword/useAddPasswordOperation";
+import { useAddPasswordTips } from "../components/tooltips/useAddPasswordTips";
+import { BaseToolProps } from "../types/tool";
+
+const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
+ const { t } = useTranslation();
+ const { setCurrentMode } = useFileContext();
+ const { selectedFiles } = useToolFileSelection();
+
+ const [collapsedPermissions, setCollapsedPermissions] = useState(true);
+
+ const addPasswordParams = useAddPasswordParameters();
+ const addPasswordOperation = useAddPasswordOperation();
+ const addPasswordTips = useAddPasswordTips();
+
+ // Endpoint validation
+ const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(addPasswordParams.getEndpointName());
+
+ useEffect(() => {
+ addPasswordOperation.resetResults();
+ onPreviewFile?.(null);
+ }, [addPasswordParams.parameters, selectedFiles]);
+
+ const handleAddPassword = async () => {
+ try {
+ await addPasswordOperation.executeOperation(
+ addPasswordParams.fullParameters,
+ selectedFiles
+ );
+ if (addPasswordOperation.files && onComplete) {
+ onComplete(addPasswordOperation.files);
+ }
+ } catch (error) {
+ if (onError) {
+ onError(error instanceof Error ? error.message : t('addPassword.error.failed', 'Add password operation failed'));
+ }
+ }
+ };
+
+ const handleThumbnailClick = (file: File) => {
+ onPreviewFile?.(file);
+ sessionStorage.setItem('previousMode', 'addPassword');
+ setCurrentMode('viewer');
+ };
+
+ const handleSettingsReset = () => {
+ addPasswordOperation.resetResults();
+ onPreviewFile?.(null);
+ setCurrentMode('addPassword');
+ };
+
+ const hasFiles = selectedFiles.length > 0;
+ const hasResults = addPasswordOperation.files.length > 0 || addPasswordOperation.downloadUrl !== null;
+ const filesCollapsed = hasFiles;
+ const passwordsCollapsed = hasResults;
+ const permissionsCollapsed = collapsedPermissions || hasResults;
+
+ const previewResults = useMemo(() =>
+ addPasswordOperation.files?.map((file, index) => ({
+ file,
+ thumbnail: addPasswordOperation.thumbnails[index]
+ })) || [],
+ [addPasswordOperation.files, addPasswordOperation.thumbnails]
+ );
+
+ return (
+
+
+ {/* Files Step */}
+
+
+
+
+ {/* Passwords & Encryption Step */}
+
+
+
+
+ {/* Permissions Step */}
+ setCollapsedPermissions(!collapsedPermissions)}
+ >
+
+
+
+
+
+
+
+ {/* Results Step */}
+
+
+ {addPasswordOperation.status && (
+ {addPasswordOperation.status}
+ )}
+
+
+
+ {addPasswordOperation.downloadUrl && (
+ }
+ color="green"
+ fullWidth
+ mb="md"
+ >
+ {t("download", "Download")}
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+export default AddPassword;
diff --git a/frontend/src/tools/ChangePermissions.tsx b/frontend/src/tools/ChangePermissions.tsx
new file mode 100644
index 000000000..58cb32474
--- /dev/null
+++ b/frontend/src/tools/ChangePermissions.tsx
@@ -0,0 +1,170 @@
+import { useEffect, useMemo } 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 { useFileContext } from "../contexts/FileContext";
+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 ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
+
+import { useChangePermissionsParameters } from "../hooks/tools/changePermissions/useChangePermissionsParameters";
+import { useChangePermissionsOperation } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
+import { useChangePermissionsTips } from "../components/tooltips/useChangePermissionsTips";
+import { BaseToolProps } from "../types/tool";
+
+const ChangePermissions = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
+ const { t } = useTranslation();
+ const { setCurrentMode } = useFileContext();
+ const { selectedFiles } = useToolFileSelection();
+
+ const changePermissionsParams = useChangePermissionsParameters();
+ const changePermissionsOperation = useChangePermissionsOperation();
+ const changePermissionsTips = useChangePermissionsTips();
+
+ // Endpoint validation
+ const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(changePermissionsParams.getEndpointName());
+
+ useEffect(() => {
+ changePermissionsOperation.resetResults();
+ onPreviewFile?.(null);
+ }, [changePermissionsParams.parameters, selectedFiles]);
+
+ const handleChangePermissions = async () => {
+ try {
+ await changePermissionsOperation.executeOperation(
+ changePermissionsParams.parameters,
+ selectedFiles
+ );
+ if (changePermissionsOperation.files && onComplete) {
+ onComplete(changePermissionsOperation.files);
+ }
+ } catch (error) {
+ if (onError) {
+ onError(error instanceof Error ? error.message : t('changePermissions.error.failed', 'Change permissions operation failed'));
+ }
+ }
+ };
+
+ const handleThumbnailClick = (file: File) => {
+ onPreviewFile?.(file);
+ sessionStorage.setItem('previousMode', 'changePermissions');
+ setCurrentMode('viewer');
+ };
+
+ const handleSettingsReset = () => {
+ changePermissionsOperation.resetResults();
+ onPreviewFile?.(null);
+ setCurrentMode('changePermissions');
+ };
+
+ const hasFiles = selectedFiles.length > 0;
+ const hasResults = changePermissionsOperation.files.length > 0 || changePermissionsOperation.downloadUrl !== null;
+ const filesCollapsed = hasFiles;
+ const settingsCollapsed = hasResults;
+
+ const previewResults = useMemo(() =>
+ changePermissionsOperation.files?.map((file, index) => ({
+ file,
+ thumbnail: changePermissionsOperation.thumbnails[index]
+ })) || [],
+ [changePermissionsOperation.files, changePermissionsOperation.thumbnails]
+ );
+
+ return (
+
+
+ {/* Files Step */}
+
+
+
+
+ {/* Settings Step */}
+
+
+
+
+
+
+
+
+ {/* Results Step */}
+
+
+ {changePermissionsOperation.status && (
+ {changePermissionsOperation.status}
+ )}
+
+
+
+ {changePermissionsOperation.downloadUrl && (
+ }
+ color="green"
+ fullWidth
+ mb="md"
+ >
+ {t("download", "Download")}
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+export default ChangePermissions;
diff --git a/frontend/src/tools/Compress.tsx b/frontend/src/tools/Compress.tsx
index f4b50b264..0adc2e23b 100644
--- a/frontend/src/tools/Compress.tsx
+++ b/frontend/src/tools/Compress.tsx
@@ -17,7 +17,7 @@ import CompressSettings from "../components/tools/compress/CompressSettings";
import { useCompressParameters } from "../hooks/tools/compress/useCompressParameters";
import { useCompressOperation } from "../hooks/tools/compress/useCompressOperation";
import { BaseToolProps } from "../types/tool";
-import { CompressTips } from "../components/tooltips/CompressTips";
+import { useCompressTips } from "../components/tooltips/useCompressTips";
const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
@@ -26,7 +26,7 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const compressParams = useCompressParameters();
const compressOperation = useCompressOperation();
- const compressTips = CompressTips();
+ const compressTips = useCompressTips();
// Endpoint validation
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("compress-pdf");
diff --git a/frontend/src/tools/OCR.tsx b/frontend/src/tools/OCR.tsx
index 89da5be87..eeb631f6d 100644
--- a/frontend/src/tools/OCR.tsx
+++ b/frontend/src/tools/OCR.tsx
@@ -18,7 +18,7 @@ import AdvancedOCRSettings from "../components/tools/ocr/AdvancedOCRSettings";
import { useOCRParameters } from "../hooks/tools/ocr/useOCRParameters";
import { useOCROperation } from "../hooks/tools/ocr/useOCROperation";
import { BaseToolProps } from "../types/tool";
-import { OcrTips } from "../components/tooltips/OCRTips";
+import { useOCRTips } from "../components/tooltips/useOCRTips";
const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
@@ -27,7 +27,7 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const ocrParams = useOCRParameters();
const ocrOperation = useOCROperation();
- const ocrTips = OcrTips();
+ const ocrTips = useOCRTips();
// Step expansion state management
const [expandedStep, setExpandedStep] = useState<'files' | 'settings' | 'advanced' | null>('files');
@@ -105,7 +105,7 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
isCollapsed={hasFiles ? filesCollapsed : false}
isCompleted={hasFiles}
onCollapsedClick={undefined}
- completedMessage={hasFiles && filesCollapsed ?
+ completedMessage={hasFiles && filesCollapsed ?
selectedFiles.length === 1
? `Selected: ${selectedFiles[0].name}`
: `Selected: ${selectedFiles.length} files`
@@ -215,4 +215,4 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
);
}
-export default OCR;
\ No newline at end of file
+export default OCR;
diff --git a/frontend/src/tools/Sanitize.tsx b/frontend/src/tools/Sanitize.tsx
index 6a4e9ac49..bc3fcd67a 100644
--- a/frontend/src/tools/Sanitize.tsx
+++ b/frontend/src/tools/Sanitize.tsx
@@ -70,7 +70,7 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
return (
-
+
{/* Files Step */}