mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-08-16 13:47:28 +02:00
Implement 'Add Password' and 'Change Permissions' tools in V2 (#4195)
# Description of Changes Implement Add Password and Change Permissions tools in V2 (both in one because Change Permissions is a fake endpoint which just calls Add Password behind the scenes). --------- Co-authored-by: James <james@crosscourtanalytics.com>
This commit is contained in:
parent
0ea4410dd3
commit
ecf30d1028
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 }) => (
|
||||
<MantineProvider>{children}</MantineProvider>
|
||||
);
|
||||
|
||||
describe('AddPasswordSettings', () => {
|
||||
const mockOnParameterChange = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render password input fields', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<AddPasswordSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// 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(
|
||||
<TestWrapper>
|
||||
<AddPasswordSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Should render key length select input
|
||||
expect(screen.getByRole('textbox', { name: /keyLength/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render main component sections', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<AddPasswordSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// 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(
|
||||
<TestWrapper>
|
||||
<AddPasswordSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// 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(
|
||||
<TestWrapper>
|
||||
<AddPasswordSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// 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(
|
||||
<TestWrapper>
|
||||
<AddPasswordSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
disabled={true}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// 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(
|
||||
<TestWrapper>
|
||||
<AddPasswordSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
disabled={false}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// 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(
|
||||
<TestWrapper>
|
||||
<AddPasswordSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// 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(
|
||||
<TestWrapper>
|
||||
<AddPasswordSettings
|
||||
parameters={{ ...defaultParameters, keyLength }}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByText(expectedLabel)).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -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 (
|
||||
<Stack gap="md">
|
||||
{/* Password Settings */}
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>{t('addPassword.passwords.title', 'Passwords')}</Text>
|
||||
<PasswordInput
|
||||
label={t('addPassword.passwords.user.label', 'User Password')}
|
||||
placeholder={t('addPassword.passwords.user.placeholder', 'Enter user password')}
|
||||
value={parameters.password}
|
||||
onChange={(e) => onParameterChange('password', e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<PasswordInput
|
||||
label={t('addPassword.passwords.owner.label', 'Owner Password')}
|
||||
placeholder={t('addPassword.passwords.owner.placeholder', 'Enter owner password')}
|
||||
value={parameters.ownerPassword}
|
||||
onChange={(e) => onParameterChange('ownerPassword', e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{/* Encryption Settings */}
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>{t('addPassword.encryption.title', 'Encryption')}</Text>
|
||||
<Select
|
||||
label={t('addPassword.encryption.keyLength.label', 'Key Length')}
|
||||
value={parameters.keyLength.toString()}
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
onParameterChange('keyLength', parseInt(value));
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
data={[
|
||||
{ value: '40', label: t('addPassword.encryption.keyLength.40bit', '40-bit (Low)') },
|
||||
{ value: '128', label: t('addPassword.encryption.keyLength.128bit', '128-bit (Standard)') },
|
||||
{ value: '256', label: t('addPassword.encryption.keyLength.256bit', '256-bit (High)') }
|
||||
]}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPasswordSettings;
|
@ -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 }) => (
|
||||
<MantineProvider>{children}</MantineProvider>
|
||||
);
|
||||
|
||||
describe('ChangePermissionsSettings', () => {
|
||||
const mockOnParameterChange = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render section title', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChangePermissionsSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByText('mock-changePermissions.restrictions.title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render all permission checkboxes', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChangePermissionsSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Should render checkboxes for all permission types
|
||||
const permissionKeys = Object.keys(defaultParameters) as Array<keyof ChangePermissionsParameters>;
|
||||
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(
|
||||
<TestWrapper>
|
||||
<ChangePermissionsSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const permissionKeys = Object.keys(defaultParameters) as Array<keyof ChangePermissionsParameters>;
|
||||
|
||||
permissionKeys.forEach(permission => {
|
||||
expect(screen.getByText(`mock-changePermissions.restrictions.${permission}.label`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should show checkboxes as unchecked by default', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChangePermissionsSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
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(
|
||||
<TestWrapper>
|
||||
<ChangePermissionsSettings
|
||||
parameters={checkedParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// 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(
|
||||
<TestWrapper>
|
||||
<ChangePermissionsSettings
|
||||
parameters={testParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const assemblyCheckbox = screen.getByLabelText('mock-changePermissions.restrictions.preventAssembly.label');
|
||||
fireEvent.click(assemblyCheckbox);
|
||||
|
||||
expect(mockOnParameterChange).toHaveBeenCalledWith('preventAssembly', expectedValue);
|
||||
});
|
||||
|
||||
test('should handle multiple checkbox interactions', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChangePermissionsSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// 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(
|
||||
<TestWrapper>
|
||||
<ChangePermissionsSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const checkboxes = screen.getAllByRole('checkbox') as HTMLInputElement[];
|
||||
checkboxes.forEach(checkbox => {
|
||||
expect(checkbox.disabled).toBe(expectedState);
|
||||
});
|
||||
});
|
||||
|
||||
test('should call translation function with correct keys', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChangePermissionsSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Verify that translation keys are being called
|
||||
expect(mockT).toHaveBeenCalledWith('changePermissions.restrictions.title', 'Document Restrictions');
|
||||
|
||||
const permissionKeys = Object.keys(defaultParameters) as Array<keyof ChangePermissionsParameters>;
|
||||
permissionKeys.forEach(permission => {
|
||||
expect(mockT).toHaveBeenCalledWith(`changePermissions.restrictions.${permission}.label`, permission);
|
||||
});
|
||||
});
|
||||
|
||||
test.each(Object.keys(defaultParameters) as Array<keyof ChangePermissionsParameters>)('should handle %s permission type individually', (permission) => {
|
||||
const testParameters: ChangePermissionsParameters = {
|
||||
...defaultParameters,
|
||||
[permission]: true
|
||||
};
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChangePermissionsSettings
|
||||
parameters={testParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const checkbox = screen.getByLabelText(`mock-changePermissions.restrictions.${permission}.label`) as HTMLInputElement;
|
||||
expect(checkbox.checked).toBe(true);
|
||||
});
|
||||
});
|
@ -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 (
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>{t('changePermissions.restrictions.title', 'Document Restrictions')}</Text>
|
||||
|
||||
<Stack gap="xs">
|
||||
{(Object.keys(parameters) as Array<keyof ChangePermissionsParameters>).map((key) => (
|
||||
<Checkbox
|
||||
key={key}
|
||||
label={t(`changePermissions.restrictions.${key}.label`, key)}
|
||||
checked={parameters[key]}
|
||||
onChange={(e) => onParameterChange(key, e.target.checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePermissionsSettings;
|
@ -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;
|
||||
|
35
frontend/src/components/tooltips/useAddPasswordTips.ts
Normal file
35
frontend/src/components/tooltips/useAddPasswordTips.ts
Normal file
@ -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."),
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
18
frontend/src/components/tooltips/useChangePermissionsTips.ts
Normal file
18
frontend/src/components/tooltips/useChangePermissionsTips.ts
Normal file
@ -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.")
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
@ -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 => {
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
};
|
@ -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 => {
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
};
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
@ -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<AddPasswordFullParameters> => mockUseToolOperation.mock.calls[0][0];
|
||||
|
||||
const mockToolOperationReturn: ToolOperationHook<unknown> = {
|
||||
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);
|
||||
});
|
||||
});
|
@ -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<AddPasswordFullParameters>({
|
||||
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.'))
|
||||
});
|
||||
};
|
@ -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<keyof ChangePermissionsParameters>)('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);
|
||||
});
|
||||
});
|
@ -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: <K extends keyof AddPasswordParameters>(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<AddPasswordParameters>(defaultParameters);
|
||||
const permissions = useChangePermissionsParameters();
|
||||
const fullParameters: AddPasswordFullParameters = {
|
||||
...parameters,
|
||||
permissions: permissions.parameters,
|
||||
};
|
||||
|
||||
const updateParameter = <K extends keyof AddPasswordParameters>(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,
|
||||
};
|
||||
};
|
@ -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<ChangePermissionsParameters> => mockUseToolOperation.mock.calls[0][0];
|
||||
|
||||
const mockToolOperationReturn: ToolOperationHook<unknown> = {
|
||||
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<keyof ChangePermissionsParameters>).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);
|
||||
});
|
||||
});
|
@ -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.')
|
||||
)
|
||||
});
|
||||
};
|
@ -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<keyof ChangePermissionsParameters>;
|
||||
|
||||
// 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<keyof ChangePermissionsParameters>;
|
||||
permissionKeys.forEach(key => {
|
||||
result.current.updateParameter(key, true);
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.validateParameters()).toBe(true);
|
||||
});
|
||||
});
|
@ -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<ChangePermissionsParameters>(defaultParameters);
|
||||
|
||||
const updateParameter = <K extends keyof ChangePermissionsParameters>(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,
|
||||
};
|
||||
};
|
@ -36,7 +36,7 @@ export interface ToolOperationConfig<TParams = void> {
|
||||
* - (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;
|
||||
|
@ -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';
|
||||
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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<string, ToolDefinition> = {
|
||||
description: "Remove potentially harmful elements from PDF files",
|
||||
endpoints: ["sanitize-pdf"]
|
||||
},
|
||||
addPassword: {
|
||||
id: "addPassword",
|
||||
icon: <LockIcon />,
|
||||
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: <LockIcon />,
|
||||
component: React.lazy(() => import("../tools/ChangePermissions")),
|
||||
maxFiles: -1,
|
||||
category: "security",
|
||||
description: "Change document restrictions and permissions",
|
||||
endpoints: ["add-password"]
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
189
frontend/src/tools/AddPassword.tsx
Normal file
189
frontend/src/tools/AddPassword.tsx
Normal file
@ -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 (
|
||||
<ToolStepContainer>
|
||||
<Stack gap="sm" h="94vh" p="sm" style={{ overflow: 'auto' }}>
|
||||
{/* Files Step */}
|
||||
<ToolStep
|
||||
title={t('files.title', 'Files')}
|
||||
isVisible={true}
|
||||
isCollapsed={filesCollapsed}
|
||||
isCompleted={filesCollapsed}
|
||||
completedMessage={hasFiles ?
|
||||
selectedFiles.length === 1
|
||||
? t('files.selected.single', 'Selected: {{filename}}', { filename: selectedFiles[0].name })
|
||||
: t('files.selected.multiple', 'Selected: {{count}} files', { count: selectedFiles.length })
|
||||
: undefined}
|
||||
>
|
||||
<FileStatusIndicator
|
||||
selectedFiles={selectedFiles}
|
||||
placeholder={t('files.placeholder', 'Select a PDF file in the main view to get started')}
|
||||
/>
|
||||
</ToolStep>
|
||||
|
||||
{/* Passwords & Encryption Step */}
|
||||
<ToolStep
|
||||
title={t('addPassword.passwords.stepTitle', 'Passwords & Encryption')}
|
||||
isVisible={hasFiles}
|
||||
isCollapsed={passwordsCollapsed}
|
||||
isCompleted={passwordsCollapsed}
|
||||
onCollapsedClick={hasResults ? handleSettingsReset : undefined}
|
||||
completedMessage={passwordsCollapsed ? t('addPassword.passwords.completed', 'Passwords configured') : undefined}
|
||||
tooltip={addPasswordTips}
|
||||
>
|
||||
<AddPasswordSettings
|
||||
parameters={addPasswordParams.parameters}
|
||||
onParameterChange={addPasswordParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
</ToolStep>
|
||||
|
||||
{/* Permissions Step */}
|
||||
<ToolStep
|
||||
title={t('addPassword.permissions.stepTitle', 'Document Permissions')}
|
||||
isVisible={hasFiles}
|
||||
isCollapsed={permissionsCollapsed}
|
||||
isCompleted={permissionsCollapsed}
|
||||
onCollapsedClick={hasResults ? handleSettingsReset : () => setCollapsedPermissions(!collapsedPermissions)}
|
||||
>
|
||||
<ChangePermissionsSettings
|
||||
parameters={addPasswordParams.permissions.parameters}
|
||||
onParameterChange={addPasswordParams.permissions.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
</ToolStep>
|
||||
|
||||
<Box mt="md">
|
||||
<OperationButton
|
||||
onClick={handleAddPassword}
|
||||
isLoading={addPasswordOperation.isLoading}
|
||||
disabled={!addPasswordParams.validateParameters() || !hasFiles || !endpointEnabled}
|
||||
loadingText={t('loading')}
|
||||
submitText={t('addPassword.submit', 'Encrypt')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Results Step */}
|
||||
<ToolStep
|
||||
title={t('results.title', 'Results')}
|
||||
isVisible={hasResults}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{addPasswordOperation.status && (
|
||||
<Text size="sm" c="dimmed">{addPasswordOperation.status}</Text>
|
||||
)}
|
||||
|
||||
<ErrorNotification
|
||||
error={addPasswordOperation.errorMessage}
|
||||
onClose={addPasswordOperation.clearError}
|
||||
/>
|
||||
|
||||
{addPasswordOperation.downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={addPasswordOperation.downloadUrl}
|
||||
download={addPasswordOperation.downloadFilename}
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
fullWidth
|
||||
mb="md"
|
||||
>
|
||||
{t("download", "Download")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<ResultsPreview
|
||||
files={previewResults}
|
||||
onFileClick={handleThumbnailClick}
|
||||
isGeneratingThumbnails={addPasswordOperation.isGeneratingThumbnails}
|
||||
title={t('addPassword.results.title', 'Encrypted PDFs')}
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
</Stack>
|
||||
</ToolStepContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddPassword;
|
170
frontend/src/tools/ChangePermissions.tsx
Normal file
170
frontend/src/tools/ChangePermissions.tsx
Normal file
@ -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 (
|
||||
<ToolStepContainer>
|
||||
<Stack gap="sm" h="94vh" p="sm" style={{ overflow: 'auto' }}>
|
||||
{/* Files Step */}
|
||||
<ToolStep
|
||||
title={t('files.title', 'Files')}
|
||||
isVisible={true}
|
||||
isCollapsed={filesCollapsed}
|
||||
isCompleted={filesCollapsed}
|
||||
completedMessage={hasFiles ?
|
||||
selectedFiles.length === 1
|
||||
? t('files.selected.single', 'Selected: {{filename}}', { filename: selectedFiles[0].name })
|
||||
: t('files.selected.multiple', 'Selected: {{count}} files', { count: selectedFiles.length })
|
||||
: undefined}
|
||||
>
|
||||
<FileStatusIndicator
|
||||
selectedFiles={selectedFiles}
|
||||
placeholder={t('files.placeholder', 'Select a PDF file in the main view to get started')}
|
||||
/>
|
||||
</ToolStep>
|
||||
|
||||
{/* Settings Step */}
|
||||
<ToolStep
|
||||
title={t('settings.title', 'Settings')}
|
||||
isVisible={hasFiles}
|
||||
isCollapsed={settingsCollapsed}
|
||||
isCompleted={settingsCollapsed}
|
||||
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
|
||||
completedMessage={settingsCollapsed ? t('changePermissions.completed', 'Permissions changed') : undefined}
|
||||
tooltip={changePermissionsTips}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<ChangePermissionsSettings
|
||||
parameters={changePermissionsParams.parameters}
|
||||
onParameterChange={changePermissionsParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
|
||||
<OperationButton
|
||||
onClick={handleChangePermissions}
|
||||
isLoading={changePermissionsOperation.isLoading}
|
||||
disabled={!changePermissionsParams.validateParameters() || !hasFiles || !endpointEnabled}
|
||||
loadingText={t('loading')}
|
||||
submitText={t('changePermissions.submit', 'Change Permissions')}
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
|
||||
{/* Results Step */}
|
||||
<ToolStep
|
||||
title={t('results.title', 'Results')}
|
||||
isVisible={hasResults}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{changePermissionsOperation.status && (
|
||||
<Text size="sm" c="dimmed">{changePermissionsOperation.status}</Text>
|
||||
)}
|
||||
|
||||
<ErrorNotification
|
||||
error={changePermissionsOperation.errorMessage}
|
||||
onClose={changePermissionsOperation.clearError}
|
||||
/>
|
||||
|
||||
{changePermissionsOperation.downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={changePermissionsOperation.downloadUrl}
|
||||
download={changePermissionsOperation.downloadFilename}
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
fullWidth
|
||||
mb="md"
|
||||
>
|
||||
{t("download", "Download")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<ResultsPreview
|
||||
files={previewResults}
|
||||
onFileClick={handleThumbnailClick}
|
||||
isGeneratingThumbnails={changePermissionsOperation.isGeneratingThumbnails}
|
||||
title={t('changePermissions.results.title', 'Modified PDFs')}
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
</Stack>
|
||||
</ToolStepContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangePermissions;
|
@ -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");
|
||||
|
@ -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;
|
||||
export default OCR;
|
||||
|
@ -70,7 +70,7 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
|
||||
return (
|
||||
<ToolStepContainer>
|
||||
<Stack gap="sm" p="sm" style={{ height: '100vh', overflow: 'auto' }}>
|
||||
<Stack gap="sm" h="94vh" p="sm" style={{ overflow: 'auto' }}>
|
||||
{/* Files Step */}
|
||||
<ToolStep
|
||||
title={t('sanitize.steps.files', 'Files')}
|
||||
|
@ -5,7 +5,18 @@
|
||||
import { ProcessedFile } from './processing';
|
||||
import { PDFDocument, PDFPage, PageOperation } from './pageEditor';
|
||||
|
||||
export type ModeType = 'viewer' | 'pageEditor' | 'fileEditor' | 'merge' | 'split' | 'compress' | 'ocr' | 'convert' | 'sanitize';
|
||||
export type ModeType =
|
||||
| 'viewer'
|
||||
| 'pageEditor'
|
||||
| 'fileEditor'
|
||||
| 'merge'
|
||||
| 'split'
|
||||
| 'compress'
|
||||
| 'ocr'
|
||||
| 'convert'
|
||||
| 'sanitize'
|
||||
| 'addPassword'
|
||||
| 'changePermissions';
|
||||
|
||||
export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user