Merge branch 'feature/v2/embed-pdf' of https://github.com/Stirling-Tools/Stirling-PDF into feature/v2/embed-pdf
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import ButtonSelector from './ButtonSelector';
|
||||
|
||||
// Wrapper component to provide Mantine context
|
||||
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<MantineProvider>{children}</MantineProvider>
|
||||
);
|
||||
|
||||
describe('ButtonSelector', () => {
|
||||
const mockOnChange = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render all options as buttons', () => {
|
||||
const options = [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
label="Test Label"
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test Label')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should highlight selected button with filled variant', () => {
|
||||
const options = [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
label="Selection Label"
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const selectedButton = screen.getByRole('button', { name: 'Option 1' });
|
||||
const unselectedButton = screen.getByRole('button', { name: 'Option 2' });
|
||||
|
||||
// Check data-variant attribute for filled/outline
|
||||
expect(selectedButton).toHaveAttribute('data-variant', 'filled');
|
||||
expect(unselectedButton).toHaveAttribute('data-variant', 'outline');
|
||||
expect(screen.getByText('Selection Label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call onChange when button is clicked', () => {
|
||||
const options = [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Option 2' }));
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('option2');
|
||||
});
|
||||
|
||||
test('should handle undefined value (no selection)', () => {
|
||||
const options = [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value={undefined}
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Both buttons should be outlined when no value is selected
|
||||
const button1 = screen.getByRole('button', { name: 'Option 1' });
|
||||
const button2 = screen.getByRole('button', { name: 'Option 2' });
|
||||
|
||||
expect(button1).toHaveAttribute('data-variant', 'outline');
|
||||
expect(button2).toHaveAttribute('data-variant', 'outline');
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
description: 'disable buttons when disabled prop is true',
|
||||
options: [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
],
|
||||
globalDisabled: true,
|
||||
expectedStates: [true, true],
|
||||
},
|
||||
{
|
||||
description: 'disable individual options when option.disabled is true',
|
||||
options: [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2', disabled: true },
|
||||
],
|
||||
globalDisabled: false,
|
||||
expectedStates: [false, true],
|
||||
},
|
||||
])('should $description', ({ options, globalDisabled, expectedStates }) => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
disabled={globalDisabled}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
options.forEach((option, index) => {
|
||||
const button = screen.getByRole('button', { name: option.label });
|
||||
expect(button).toHaveProperty('disabled', expectedStates[index]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should not call onChange when disabled button is clicked', () => {
|
||||
const options = [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2', disabled: true },
|
||||
];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Option 2' }));
|
||||
|
||||
expect(mockOnChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not apply fullWidth styling when fullWidth is false', () => {
|
||||
const options = [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
fullWidth={false}
|
||||
label="Layout Label"
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Option 1' });
|
||||
expect(button).not.toHaveStyle({ flex: '1' });
|
||||
expect(screen.getByText('Layout Label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not render label element when not provided', () => {
|
||||
const options = [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
];
|
||||
|
||||
const { container } = render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Should render buttons
|
||||
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
||||
|
||||
// Stack should only contain the Group (buttons), no Text element for label
|
||||
const stackElement = container.querySelector('[class*="mantine-Stack-root"]');
|
||||
expect(stackElement?.children).toHaveLength(1); // Only the Group, no label Text
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Button, Group, Stack, Text } from "@mantine/core";
|
||||
|
||||
export interface ButtonOption<T> {
|
||||
value: T;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface ButtonSelectorProps<T> {
|
||||
value: T | undefined;
|
||||
onChange: (value: T) => void;
|
||||
options: ButtonOption<T>[];
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const ButtonSelector = <T extends string>({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
label = undefined,
|
||||
disabled = false,
|
||||
fullWidth = true,
|
||||
}: ButtonSelectorProps<T>) => {
|
||||
return (
|
||||
<Stack gap='var(--mantine-spacing-sm)'>
|
||||
{/* Label (if it exists) */}
|
||||
{label && <Text style={{
|
||||
fontSize: "var(--mantine-font-size-sm)",
|
||||
lineHeight: "var(--mantine-line-height-sm)",
|
||||
fontWeight: "var(--font-weight-medium)",
|
||||
}}>{label}</Text>}
|
||||
|
||||
{/* Buttons */}
|
||||
<Group gap='4px'>
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
variant={value === option.value ? 'filled' : 'outline'}
|
||||
color={value === option.value ? 'var(--color-primary-500)' : 'var(--text-muted)'}
|
||||
onClick={() => onChange(option.value)}
|
||||
disabled={disabled || option.disabled}
|
||||
style={{
|
||||
flex: fullWidth ? 1 : undefined,
|
||||
height: 'auto',
|
||||
minHeight: '2.5rem',
|
||||
fontSize: 'var(--mantine-font-size-sm)'
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonSelector;
|
||||
@@ -4,7 +4,7 @@ import { AddPasswordParameters } from "../../../hooks/tools/addPassword/useAddPa
|
||||
|
||||
interface AddPasswordSettingsProps {
|
||||
parameters: AddPasswordParameters;
|
||||
onParameterChange: (key: keyof AddPasswordParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof AddPasswordParameters>(key: K, value: AddPasswordParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button, Stack } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ButtonSelector from "../../shared/ButtonSelector";
|
||||
|
||||
interface WatermarkTypeSettingsProps {
|
||||
watermarkType?: 'text' | 'image';
|
||||
@@ -11,32 +11,21 @@ const WatermarkTypeSettings = ({ watermarkType, onWatermarkTypeChange, disabled
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<Button
|
||||
variant={watermarkType === 'text' ? 'filled' : 'outline'}
|
||||
color={watermarkType === 'text' ? 'blue' : 'var(--text-muted)'}
|
||||
onClick={() => onWatermarkTypeChange('text')}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
{t('watermark.watermarkType.text', 'Text')}
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant={watermarkType === 'image' ? 'filled' : 'outline'}
|
||||
color={watermarkType === 'image' ? 'blue' : 'var(--text-muted)'}
|
||||
onClick={() => onWatermarkTypeChange('image')}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
{t('watermark.watermarkType.image', 'Image')}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
<ButtonSelector
|
||||
value={watermarkType}
|
||||
onChange={onWatermarkTypeChange}
|
||||
options={[
|
||||
{
|
||||
value: 'text',
|
||||
label: t('watermark.watermarkType.text', 'Text'),
|
||||
},
|
||||
{
|
||||
value: 'image',
|
||||
label: t('watermark.watermarkType.image', 'Image'),
|
||||
},
|
||||
]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import AdjustPageScaleSettings from './AdjustPageScaleSettings';
|
||||
import { AdjustPageScaleParameters, PageSize } from '../../../hooks/tools/adjustPageScale/useAdjustPageScaleParameters';
|
||||
|
||||
// Mock useTranslation with predictable return values
|
||||
const mockT = vi.fn((key: string, fallback?: string) => fallback || `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('AdjustPageScaleSettings', () => {
|
||||
const defaultParameters: AdjustPageScaleParameters = {
|
||||
scaleFactor: 1.0,
|
||||
pageSize: PageSize.KEEP,
|
||||
};
|
||||
|
||||
const mockOnParameterChange = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render without crashing', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<AdjustPageScaleSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Basic render test - component renders without throwing
|
||||
expect(screen.getByText('Scale Factor')).toBeInTheDocument();
|
||||
expect(screen.getByText('Target Page Size')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render with custom parameters', () => {
|
||||
const customParameters: AdjustPageScaleParameters = {
|
||||
scaleFactor: 2.5,
|
||||
pageSize: PageSize.A4,
|
||||
};
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<AdjustPageScaleSettings
|
||||
parameters={customParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Component renders successfully with custom parameters
|
||||
expect(screen.getByText('Scale Factor')).toBeInTheDocument();
|
||||
expect(screen.getByText('Target Page Size')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Stack, NumberInput, Select } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AdjustPageScaleParameters, PageSize } from "../../../hooks/tools/adjustPageScale/useAdjustPageScaleParameters";
|
||||
|
||||
interface AdjustPageScaleSettingsProps {
|
||||
parameters: AdjustPageScaleParameters;
|
||||
onParameterChange: <K extends keyof AdjustPageScaleParameters>(key: K, value: AdjustPageScaleParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const AdjustPageScaleSettings = ({ parameters, onParameterChange, disabled = false }: AdjustPageScaleSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const pageSizeOptions = [
|
||||
{ value: PageSize.KEEP, label: t('adjustPageScale.pageSize.keep', 'Keep Original Size') },
|
||||
{ value: PageSize.A0, label: 'A0' },
|
||||
{ value: PageSize.A1, label: 'A1' },
|
||||
{ value: PageSize.A2, label: 'A2' },
|
||||
{ value: PageSize.A3, label: 'A3' },
|
||||
{ value: PageSize.A4, label: 'A4' },
|
||||
{ value: PageSize.A5, label: 'A5' },
|
||||
{ value: PageSize.A6, label: 'A6' },
|
||||
{ value: PageSize.LETTER, label: t('adjustPageScale.pageSize.letter', 'Letter') },
|
||||
{ value: PageSize.LEGAL, label: t('adjustPageScale.pageSize.legal', 'Legal') },
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<NumberInput
|
||||
label={t('adjustPageScale.scaleFactor.label', 'Scale Factor')}
|
||||
value={parameters.scaleFactor}
|
||||
onChange={(value) => onParameterChange('scaleFactor', typeof value === 'number' ? value : 1.0)}
|
||||
min={0.1}
|
||||
max={10.0}
|
||||
step={0.1}
|
||||
decimalScale={2}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label={t('adjustPageScale.pageSize.label', 'Target Page Size')}
|
||||
value={parameters.pageSize}
|
||||
onChange={(value) => {
|
||||
if (value && Object.values(PageSize).includes(value as PageSize)) {
|
||||
onParameterChange('pageSize', value as PageSize);
|
||||
}
|
||||
}}
|
||||
data={pageSizeOptions}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdjustPageScaleSettings;
|
||||
@@ -4,7 +4,7 @@ import { ChangePermissionsParameters } from "../../../hooks/tools/changePermissi
|
||||
|
||||
interface ChangePermissionsSettingsProps {
|
||||
parameters: ChangePermissionsParameters;
|
||||
onParameterChange: (key: keyof ChangePermissionsParameters, value: boolean) => void;
|
||||
onParameterChange: <K extends keyof ChangePermissionsParameters>(key: K, value: ChangePermissionsParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, Stack, Text, NumberInput, Select, Divider } from "@mantine/core";
|
||||
import { Stack, Text, NumberInput, Select, Divider } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CompressParameters } from "../../../hooks/tools/compress/useCompressParameters";
|
||||
import ButtonSelector from "../../shared/ButtonSelector";
|
||||
|
||||
interface CompressSettingsProps {
|
||||
parameters: CompressParameters;
|
||||
onParameterChange: (key: keyof CompressParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof CompressParameters>(key: K, value: CompressParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -18,33 +19,16 @@ const CompressSettings = ({ parameters, onParameterChange, disabled = false }: C
|
||||
|
||||
<Divider ml='-md'></Divider>
|
||||
{/* Compression Method */}
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>Compression Method</Text>
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<Button
|
||||
variant={parameters.compressionMethod === 'quality' ? 'filled' : 'outline'}
|
||||
color={parameters.compressionMethod === 'quality' ? 'blue' : 'var(--text-muted)'}
|
||||
onClick={() => onParameterChange('compressionMethod', 'quality')}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
Quality
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant={parameters.compressionMethod === 'filesize' ? 'filled' : 'outline'}
|
||||
color={parameters.compressionMethod === 'filesize' ? 'blue' : 'var(--text-muted)'}
|
||||
onClick={() => onParameterChange('compressionMethod', 'filesize')}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
File Size
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
<ButtonSelector
|
||||
label={t('compress.method.title', 'Compression Method')}
|
||||
value={parameters.compressionMethod}
|
||||
onChange={(value) => onParameterChange('compressionMethod', value)}
|
||||
options={[
|
||||
{ value: 'quality', label: t('compress.method.quality', 'Quality') },
|
||||
{ value: 'filesize', label: t('compress.method.filesize', 'File Size') },
|
||||
]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
{/* Quality Adjustment */}
|
||||
{parameters.compressionMethod === 'quality' && (
|
||||
|
||||
@@ -5,40 +5,40 @@ import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParame
|
||||
|
||||
interface ConvertFromEmailSettingsProps {
|
||||
parameters: ConvertParameters;
|
||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ConvertFromEmailSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false
|
||||
const ConvertFromEmailSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false
|
||||
}: ConvertFromEmailSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="sm" data-testid="email-settings">
|
||||
<Text size="sm" fw={500}>{t("convert.emailOptions", "Email to PDF Options")}:</Text>
|
||||
|
||||
|
||||
<Checkbox
|
||||
label={t("convert.includeAttachments", "Include email attachments")}
|
||||
checked={parameters.emailOptions.includeAttachments}
|
||||
onChange={(event) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
includeAttachments: event.currentTarget.checked
|
||||
onChange={(event) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
includeAttachments: event.currentTarget.checked
|
||||
})}
|
||||
disabled={disabled}
|
||||
data-testid="include-attachments-checkbox"
|
||||
/>
|
||||
|
||||
|
||||
{parameters.emailOptions.includeAttachments && (
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" fw={500}>{t("convert.maxAttachmentSize", "Maximum attachment size (MB)")}:</Text>
|
||||
<NumberInput
|
||||
value={parameters.emailOptions.maxAttachmentSizeMB}
|
||||
onChange={(value) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
maxAttachmentSizeMB: Number(value) || 10
|
||||
onChange={(value) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
maxAttachmentSizeMB: Number(value) || 10
|
||||
})}
|
||||
min={1}
|
||||
max={100}
|
||||
@@ -48,24 +48,24 @@ const ConvertFromEmailSettings = ({
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
|
||||
<Checkbox
|
||||
label={t("convert.includeAllRecipients", "Include CC and BCC recipients in header")}
|
||||
checked={parameters.emailOptions.includeAllRecipients}
|
||||
onChange={(event) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
includeAllRecipients: event.currentTarget.checked
|
||||
onChange={(event) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
includeAllRecipients: event.currentTarget.checked
|
||||
})}
|
||||
disabled={disabled}
|
||||
data-testid="include-all-recipients-checkbox"
|
||||
/>
|
||||
|
||||
|
||||
<Checkbox
|
||||
label={t("convert.downloadHtml", "Download HTML intermediate file instead of PDF")}
|
||||
checked={parameters.emailOptions.downloadHtml}
|
||||
onChange={(event) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
downloadHtml: event.currentTarget.checked
|
||||
onChange={(event) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
downloadHtml: event.currentTarget.checked
|
||||
})}
|
||||
disabled={disabled}
|
||||
data-testid="download-html-checkbox"
|
||||
@@ -74,4 +74,4 @@ const ConvertFromEmailSettings = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ConvertFromEmailSettings;
|
||||
export default ConvertFromEmailSettings;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParame
|
||||
|
||||
interface ConvertFromImageSettingsProps {
|
||||
parameters: ConvertParameters;
|
||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,28 +5,28 @@ import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParame
|
||||
|
||||
interface ConvertFromWebSettingsProps {
|
||||
parameters: ConvertParameters;
|
||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ConvertFromWebSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false
|
||||
const ConvertFromWebSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false
|
||||
}: ConvertFromWebSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="sm" data-testid="web-settings">
|
||||
<Text size="sm" fw={500}>{t("convert.webOptions", "Web to PDF Options")}:</Text>
|
||||
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" fw={500}>{t("convert.zoomLevel", "Zoom Level")}:</Text>
|
||||
<NumberInput
|
||||
value={parameters.htmlOptions.zoomLevel}
|
||||
onChange={(value) => onParameterChange('htmlOptions', {
|
||||
...parameters.htmlOptions,
|
||||
zoomLevel: Number(value) || 1.0
|
||||
onChange={(value) => onParameterChange('htmlOptions', {
|
||||
...parameters.htmlOptions,
|
||||
zoomLevel: Number(value) || 1.0
|
||||
})}
|
||||
min={0.1}
|
||||
max={3.0}
|
||||
@@ -36,9 +36,9 @@ const ConvertFromWebSettings = ({
|
||||
/>
|
||||
<Slider
|
||||
value={parameters.htmlOptions.zoomLevel}
|
||||
onChange={(value) => onParameterChange('htmlOptions', {
|
||||
...parameters.htmlOptions,
|
||||
zoomLevel: value
|
||||
onChange={(value) => onParameterChange('htmlOptions', {
|
||||
...parameters.htmlOptions,
|
||||
zoomLevel: value
|
||||
})}
|
||||
min={0.1}
|
||||
max={3.0}
|
||||
@@ -51,4 +51,4 @@ const ConvertFromWebSettings = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ConvertFromWebSettings;
|
||||
export default ConvertFromWebSettings;
|
||||
|
||||
@@ -26,7 +26,7 @@ import { StirlingFile } from "../../../types/fileContext";
|
||||
|
||||
interface ConvertSettingsProps {
|
||||
parameters: ConvertParameters;
|
||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
|
||||
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
||||
selectedFiles: StirlingFile[];
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParame
|
||||
|
||||
interface ConvertToImageSettingsProps {
|
||||
parameters: ConvertParameters;
|
||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,16 +7,16 @@ import { StirlingFile } from '../../../types/fileContext';
|
||||
|
||||
interface ConvertToPdfaSettingsProps {
|
||||
parameters: ConvertParameters;
|
||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
|
||||
selectedFiles: StirlingFile[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ConvertToPdfaSettings = ({
|
||||
parameters,
|
||||
const ConvertToPdfaSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
selectedFiles,
|
||||
disabled = false
|
||||
disabled = false
|
||||
}: ConvertToPdfaSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { hasDigitalSignatures, isChecking } = usePdfSignatureDetection(selectedFiles);
|
||||
@@ -29,7 +29,7 @@ const ConvertToPdfaSettings = ({
|
||||
return (
|
||||
<Stack gap="sm" data-testid="pdfa-settings">
|
||||
<Text size="sm" fw={500}>{t("convert.pdfaOptions", "PDF/A Options")}:</Text>
|
||||
|
||||
|
||||
{hasDigitalSignatures && (
|
||||
<Alert color="yellow">
|
||||
<Text size="sm">
|
||||
@@ -37,14 +37,14 @@ const ConvertToPdfaSettings = ({
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" fw={500}>{t("convert.outputFormat", "Output Format")}:</Text>
|
||||
<Select
|
||||
value={parameters.pdfaOptions.outputFormat}
|
||||
onChange={(value) => onParameterChange('pdfaOptions', {
|
||||
...parameters.pdfaOptions,
|
||||
outputFormat: value || 'pdfa-1'
|
||||
onChange={(value) => onParameterChange('pdfaOptions', {
|
||||
...parameters.pdfaOptions,
|
||||
outputFormat: value || 'pdfa-1'
|
||||
})}
|
||||
data={pdfaFormatOptions}
|
||||
disabled={disabled || isChecking}
|
||||
@@ -58,4 +58,4 @@ const ConvertToPdfaSettings = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ConvertToPdfaSettings;
|
||||
export default ConvertToPdfaSettings;
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import MergeFileSorter from './MergeFileSorter';
|
||||
|
||||
// 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('MergeFileSorter', () => {
|
||||
const mockOnSortFiles = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render sort options dropdown, direction toggle, and sort button', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MergeFileSorter onSortFiles={mockOnSortFiles} />
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Should have a select dropdown (Mantine Select uses textbox role)
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
|
||||
// Should have direction toggle button
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons).toHaveLength(2); // ActionIcon + Sort Button
|
||||
|
||||
// Should have sort button with text
|
||||
expect(screen.getByText('mock-merge.sortBy.sort')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render description text', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MergeFileSorter onSortFiles={mockOnSortFiles} />
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByText('mock-merge.sortBy.description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should have filename selected by default', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MergeFileSorter onSortFiles={mockOnSortFiles} />
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const select = screen.getByRole('textbox');
|
||||
expect(select).toHaveValue('mock-merge.sortBy.filename');
|
||||
});
|
||||
|
||||
test('should show ascending direction by default', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MergeFileSorter onSortFiles={mockOnSortFiles} />
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Should show ascending arrow icon
|
||||
const directionButton = screen.getAllByRole('button')[0];
|
||||
expect(directionButton).toHaveAttribute('title', 'mock-merge.sortBy.ascending');
|
||||
});
|
||||
|
||||
test('should toggle direction when direction button is clicked', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MergeFileSorter onSortFiles={mockOnSortFiles} />
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const directionButton = screen.getAllByRole('button')[0];
|
||||
|
||||
// Initially ascending
|
||||
expect(directionButton).toHaveAttribute('title', 'mock-merge.sortBy.ascending');
|
||||
|
||||
// Click to toggle to descending
|
||||
fireEvent.click(directionButton);
|
||||
expect(directionButton).toHaveAttribute('title', 'mock-merge.sortBy.descending');
|
||||
|
||||
// Click again to toggle back to ascending
|
||||
fireEvent.click(directionButton);
|
||||
expect(directionButton).toHaveAttribute('title', 'mock-merge.sortBy.ascending');
|
||||
});
|
||||
|
||||
test('should call onSortFiles with correct parameters when sort button is clicked', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MergeFileSorter onSortFiles={mockOnSortFiles} />
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const sortButton = screen.getByText('mock-merge.sortBy.sort');
|
||||
fireEvent.click(sortButton);
|
||||
|
||||
// Should be called with default values (filename, ascending)
|
||||
expect(mockOnSortFiles).toHaveBeenCalledWith('filename', true);
|
||||
});
|
||||
|
||||
test('should call onSortFiles with dateModified when dropdown is changed', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MergeFileSorter onSortFiles={mockOnSortFiles} />
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Open the dropdown by clicking on the current selected value
|
||||
const currentSelection = screen.getByText('mock-merge.sortBy.filename');
|
||||
fireEvent.mouseDown(currentSelection);
|
||||
|
||||
// Click on the dateModified option
|
||||
const dateModifiedOption = screen.getByText('mock-merge.sortBy.dateModified');
|
||||
fireEvent.click(dateModifiedOption);
|
||||
|
||||
const sortButton = screen.getByText('mock-merge.sortBy.sort');
|
||||
fireEvent.click(sortButton);
|
||||
|
||||
expect(mockOnSortFiles).toHaveBeenCalledWith('dateModified', true);
|
||||
});
|
||||
|
||||
test('should call onSortFiles with descending direction when toggled', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MergeFileSorter onSortFiles={mockOnSortFiles} />
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const directionButton = screen.getAllByRole('button')[0];
|
||||
const sortButton = screen.getByText('mock-merge.sortBy.sort');
|
||||
|
||||
// Toggle to descending
|
||||
fireEvent.click(directionButton);
|
||||
|
||||
// Click sort
|
||||
fireEvent.click(sortButton);
|
||||
|
||||
expect(mockOnSortFiles).toHaveBeenCalledWith('filename', false);
|
||||
});
|
||||
|
||||
test('should handle complex user interaction sequence', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MergeFileSorter onSortFiles={mockOnSortFiles} />
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const directionButton = screen.getAllByRole('button')[0];
|
||||
const sortButton = screen.getByText('mock-merge.sortBy.sort');
|
||||
|
||||
// 1. Change to dateModified
|
||||
const currentSelection = screen.getByText('mock-merge.sortBy.filename');
|
||||
fireEvent.mouseDown(currentSelection);
|
||||
const dateModifiedOption = screen.getByText('mock-merge.sortBy.dateModified');
|
||||
fireEvent.click(dateModifiedOption);
|
||||
|
||||
// 2. Toggle to descending
|
||||
fireEvent.click(directionButton);
|
||||
|
||||
// 3. Click sort
|
||||
fireEvent.click(sortButton);
|
||||
|
||||
expect(mockOnSortFiles).toHaveBeenCalledWith('dateModified', false);
|
||||
|
||||
// 4. Toggle back to ascending
|
||||
fireEvent.click(directionButton);
|
||||
|
||||
// 5. Sort again
|
||||
fireEvent.click(sortButton);
|
||||
|
||||
expect(mockOnSortFiles).toHaveBeenCalledWith('dateModified', true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Group, Button, Text, ActionIcon, Stack, Select } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SortIcon from '@mui/icons-material/Sort';
|
||||
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
||||
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
||||
|
||||
interface MergeFileSorterProps {
|
||||
onSortFiles: (sortType: 'filename' | 'dateModified', ascending: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const MergeFileSorter: React.FC<MergeFileSorterProps> = ({
|
||||
onSortFiles,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [sortType, setSortType] = useState<'filename' | 'dateModified'>('filename');
|
||||
const [ascending, setAscending] = useState(true);
|
||||
|
||||
const sortOptions = [
|
||||
{ value: 'filename', label: t('merge.sortBy.filename', 'File Name') },
|
||||
{ value: 'dateModified', label: t('merge.sortBy.dateModified', 'Date Modified') },
|
||||
];
|
||||
|
||||
const handleSort = () => {
|
||||
onSortFiles(sortType, ascending);
|
||||
};
|
||||
|
||||
const handleDirectionToggle = () => {
|
||||
setAscending(!ascending);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
{t('merge.sortBy.description', "Files will be merged in the order they're selected. Drag to reorder or sort below.")}
|
||||
</Text>
|
||||
<Stack gap="xs">
|
||||
<Group gap="xs" align="end" justify="space-between">
|
||||
<Select
|
||||
data={sortOptions}
|
||||
value={sortType}
|
||||
onChange={(value) => setSortType(value as 'filename' | 'dateModified')}
|
||||
disabled={disabled}
|
||||
label={t('merge.sortBy.label', 'Sort By')}
|
||||
size='xs'
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="md"
|
||||
onClick={handleDirectionToggle}
|
||||
disabled={disabled}
|
||||
title={ascending ? t('merge.sortBy.ascending', 'Ascending') : t('merge.sortBy.descending', 'Descending')}
|
||||
>
|
||||
{ascending ? <ArrowUpwardIcon /> : <ArrowDownwardIcon />}
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
size="xs"
|
||||
leftSection={<SortIcon />}
|
||||
onClick={handleSort}
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
>
|
||||
{t('merge.sortBy.sort', 'Sort')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default MergeFileSorter;
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import MergeSettings from './MergeSettings';
|
||||
import { MergeParameters } from '../../../hooks/tools/merge/useMergeParameters';
|
||||
|
||||
// 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('MergeSettings', () => {
|
||||
const defaultParameters: MergeParameters = {
|
||||
removeDigitalSignature: false,
|
||||
generateTableOfContents: false,
|
||||
};
|
||||
|
||||
const mockOnParameterChange = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render both merge option checkboxes', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MergeSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Should render one checkbox for each parameter
|
||||
const expectedCheckboxCount = Object.keys(defaultParameters).length;
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
expect(checkboxes).toHaveLength(expectedCheckboxCount);
|
||||
});
|
||||
|
||||
test('should show correct initial checkbox states based on parameters', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MergeSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
|
||||
// Both checkboxes should be unchecked initially
|
||||
checkboxes.forEach(checkbox => {
|
||||
expect(checkbox).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
test('should call onParameterChange with correct parameters when checkboxes are clicked', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MergeSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
|
||||
// Click the first checkbox (removeDigitalSignature - should toggle from false to true)
|
||||
fireEvent.click(checkboxes[0]);
|
||||
expect(mockOnParameterChange).toHaveBeenCalledWith('removeDigitalSignature', true);
|
||||
|
||||
// Click the second checkbox (generateTableOfContents - should toggle from false to true)
|
||||
fireEvent.click(checkboxes[1]);
|
||||
expect(mockOnParameterChange).toHaveBeenCalledWith('generateTableOfContents', true);
|
||||
});
|
||||
|
||||
test('should call translation function with correct keys', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MergeSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Verify that translation keys are being called
|
||||
expect(mockT).toHaveBeenCalledWith('merge.removeDigitalSignature', 'Remove digital signature in the merged file?');
|
||||
expect(mockT).toHaveBeenCalledWith('merge.generateTableOfContents', 'Generate table of contents in the merged file?');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { Stack, Checkbox } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MergeParameters } from '../../../hooks/tools/merge/useMergeParameters';
|
||||
|
||||
interface MergeSettingsProps {
|
||||
parameters: MergeParameters;
|
||||
onParameterChange: <K extends keyof MergeParameters>(key: K, value: MergeParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const MergeSettings: React.FC<MergeSettingsProps> = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Checkbox
|
||||
label={t('merge.removeDigitalSignature', 'Remove digital signature in the merged file?')}
|
||||
checked={parameters.removeDigitalSignature}
|
||||
onChange={(event) => onParameterChange('removeDigitalSignature', event.currentTarget.checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
label={t('merge.generateTableOfContents', 'Generate table of contents in the merged file?')}
|
||||
checked={parameters.generateTableOfContents}
|
||||
onChange={(event) => onParameterChange('generateTableOfContents', event.currentTarget.checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default MergeSettings;
|
||||
@@ -16,7 +16,7 @@ interface AdvancedOption {
|
||||
interface AdvancedOCRSettingsProps {
|
||||
advancedOptions: string[];
|
||||
ocrRenderType?: string;
|
||||
onParameterChange: (key: keyof OCRParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof OCRParameters>(key: K, value: OCRParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ const AdvancedOCRSettings: React.FC<AdvancedOCRSettingsProps> = ({
|
||||
// Handle individual checkbox changes
|
||||
const handleCheckboxChange = (optionValue: string, checked: boolean) => {
|
||||
const option = advancedOptionsData.find(opt => opt.value === optionValue);
|
||||
|
||||
|
||||
if (option?.isSpecial) {
|
||||
// Handle special options (like compatibility mode) differently
|
||||
if (optionValue === 'compatibilityMode') {
|
||||
@@ -69,7 +69,7 @@ const AdvancedOCRSettings: React.FC<AdvancedOCRSettingsProps> = ({
|
||||
<Text size="sm" fw={500} mb="md">
|
||||
{t('ocr.settings.advancedOptions.label', 'Processing Options')}
|
||||
</Text>
|
||||
|
||||
|
||||
<Stack gap="sm">
|
||||
{advancedOptionsData.map((option) => (
|
||||
<Checkbox
|
||||
@@ -87,4 +87,4 @@ const AdvancedOCRSettings: React.FC<AdvancedOCRSettingsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedOCRSettings;
|
||||
export default AdvancedOCRSettings;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { OCRParameters } from '../../../hooks/tools/ocr/useOCRParameters';
|
||||
|
||||
interface OCRSettingsProps {
|
||||
parameters: OCRParameters;
|
||||
onParameterChange: (key: keyof OCRParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof OCRParameters>(key: K, value: OCRParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import RedactAdvancedSettings from './RedactAdvancedSettings';
|
||||
import { defaultParameters } from '../../../hooks/tools/redact/useRedactParameters';
|
||||
|
||||
// Mock useTranslation
|
||||
const mockT = vi.fn((_key: string, fallback: string) => fallback);
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: mockT })
|
||||
}));
|
||||
|
||||
// Wrapper component to provide Mantine context
|
||||
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<MantineProvider>{children}</MantineProvider>
|
||||
);
|
||||
|
||||
describe('RedactAdvancedSettings', () => {
|
||||
const mockOnParameterChange = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render all advanced settings controls', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactAdvancedSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Box Colour')).toBeInTheDocument();
|
||||
expect(screen.getByText('Custom Extra Padding')).toBeInTheDocument();
|
||||
expect(screen.getByText('Use Regex')).toBeInTheDocument();
|
||||
expect(screen.getByText('Whole Word Search')).toBeInTheDocument();
|
||||
expect(screen.getByText('Convert PDF to PDF-Image (Used to remove text behind the box)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should display current parameter values', () => {
|
||||
const customParameters = {
|
||||
...defaultParameters,
|
||||
redactColor: '#FF0000',
|
||||
customPadding: 0.5,
|
||||
useRegex: true,
|
||||
wholeWordSearch: true,
|
||||
convertPDFToImage: false,
|
||||
};
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactAdvancedSettings
|
||||
parameters={customParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Check color input value
|
||||
const colorInput = screen.getByDisplayValue('#FF0000');
|
||||
expect(colorInput).toBeInTheDocument();
|
||||
|
||||
// Check number input value
|
||||
const paddingInput = screen.getByDisplayValue('0.5');
|
||||
expect(paddingInput).toBeInTheDocument();
|
||||
|
||||
// Check checkbox states
|
||||
const useRegexCheckbox = screen.getByLabelText('Use Regex');
|
||||
const wholeWordCheckbox = screen.getByLabelText('Whole Word Search');
|
||||
const convertCheckbox = screen.getByLabelText('Convert PDF to PDF-Image (Used to remove text behind the box)');
|
||||
|
||||
expect(useRegexCheckbox).toBeChecked();
|
||||
expect(wholeWordCheckbox).toBeChecked();
|
||||
expect(convertCheckbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
test('should call onParameterChange when color is changed', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactAdvancedSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const colorInput = screen.getByDisplayValue('#000000');
|
||||
fireEvent.change(colorInput, { target: { value: '#FF0000' } });
|
||||
|
||||
expect(mockOnParameterChange).toHaveBeenCalledWith('redactColor', '#FF0000');
|
||||
});
|
||||
|
||||
test('should call onParameterChange when padding is changed', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactAdvancedSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const paddingInput = screen.getByDisplayValue('0.1');
|
||||
fireEvent.change(paddingInput, { target: { value: '0.5' } });
|
||||
|
||||
expect(mockOnParameterChange).toHaveBeenCalledWith('customPadding', 0.5);
|
||||
});
|
||||
|
||||
test('should handle invalid padding values', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactAdvancedSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const paddingInput = screen.getByDisplayValue('0.1');
|
||||
|
||||
// Simulate NumberInput onChange with invalid value (empty string)
|
||||
const numberInput = paddingInput.closest('.mantine-NumberInput-root');
|
||||
if (numberInput) {
|
||||
// Find the input and trigger change with empty value
|
||||
fireEvent.change(paddingInput, { target: { value: '' } });
|
||||
|
||||
// The component should default to 0.1 for invalid values
|
||||
expect(mockOnParameterChange).toHaveBeenCalledWith('customPadding', 0.1);
|
||||
}
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
paramName: 'useRegex' as const,
|
||||
label: 'Use Regex',
|
||||
initialValue: false,
|
||||
expectedValue: true,
|
||||
},
|
||||
{
|
||||
paramName: 'wholeWordSearch' as const,
|
||||
label: 'Whole Word Search',
|
||||
initialValue: false,
|
||||
expectedValue: true,
|
||||
},
|
||||
{
|
||||
paramName: 'convertPDFToImage' as const,
|
||||
label: 'Convert PDF to PDF-Image (Used to remove text behind the box)',
|
||||
initialValue: true,
|
||||
expectedValue: false,
|
||||
},
|
||||
])('should call onParameterChange when $paramName checkbox is toggled', ({ paramName, label, initialValue, expectedValue }) => {
|
||||
const customParameters = {
|
||||
...defaultParameters,
|
||||
[paramName]: initialValue,
|
||||
};
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactAdvancedSettings
|
||||
parameters={customParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const checkbox = screen.getByLabelText(label);
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(mockOnParameterChange).toHaveBeenCalledWith(paramName, expectedValue);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ controlType: 'color input', getValue: () => screen.getByDisplayValue('#000000') },
|
||||
{ controlType: 'padding input', getValue: () => screen.getByDisplayValue('0.1') },
|
||||
{ controlType: 'useRegex checkbox', getValue: () => screen.getByLabelText('Use Regex') },
|
||||
{ controlType: 'wholeWordSearch checkbox', getValue: () => screen.getByLabelText('Whole Word Search') },
|
||||
{ controlType: 'convertPDFToImage checkbox', getValue: () => screen.getByLabelText('Convert PDF to PDF-Image (Used to remove text behind the box)') },
|
||||
])('should disable $controlType when disabled prop is true', ({ getValue }) => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactAdvancedSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
disabled={true}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const control = getValue();
|
||||
expect(control).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should have correct padding input constraints', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactAdvancedSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// NumberInput in Mantine might not expose these attributes directly on the input element
|
||||
// Instead, check that the NumberInput component is rendered with correct placeholder
|
||||
const paddingInput = screen.getByPlaceholderText('0.1');
|
||||
expect(paddingInput).toBeInTheDocument();
|
||||
expect(paddingInput).toHaveDisplayValue('0.1');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Stack, NumberInput, ColorInput, Checkbox } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RedactParameters } from "../../../hooks/tools/redact/useRedactParameters";
|
||||
|
||||
interface RedactAdvancedSettingsProps {
|
||||
parameters: RedactParameters;
|
||||
onParameterChange: <K extends keyof RedactParameters>(key: K, value: RedactParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const RedactAdvancedSettings = ({ parameters, onParameterChange, disabled = false }: RedactAdvancedSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Box Color */}
|
||||
<ColorInput
|
||||
label={t('redact.auto.colorLabel', 'Box Colour')}
|
||||
value={parameters.redactColor}
|
||||
onChange={(value) => onParameterChange('redactColor', value)}
|
||||
disabled={disabled}
|
||||
size="sm"
|
||||
format="hex"
|
||||
/>
|
||||
|
||||
{/* Box Padding */}
|
||||
<NumberInput
|
||||
label={t('redact.auto.customPaddingLabel', 'Custom Extra Padding')}
|
||||
value={parameters.customPadding}
|
||||
onChange={(value) => onParameterChange('customPadding', typeof value === 'number' ? value : 0.1)}
|
||||
min={0}
|
||||
max={10}
|
||||
step={0.1}
|
||||
disabled={disabled}
|
||||
size="sm"
|
||||
placeholder="0.1"
|
||||
/>
|
||||
|
||||
{/* Use Regex */}
|
||||
<Checkbox
|
||||
label={t('redact.auto.useRegexLabel', 'Use Regex')}
|
||||
checked={parameters.useRegex}
|
||||
onChange={(e) => onParameterChange('useRegex', e.currentTarget.checked)}
|
||||
disabled={disabled}
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
{/* Whole Word Search */}
|
||||
<Checkbox
|
||||
label={t('redact.auto.wholeWordSearchLabel', 'Whole Word Search')}
|
||||
checked={parameters.wholeWordSearch}
|
||||
onChange={(e) => onParameterChange('wholeWordSearch', e.currentTarget.checked)}
|
||||
disabled={disabled}
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
{/* Convert PDF to PDF-Image */}
|
||||
<Checkbox
|
||||
label={t('redact.auto.convertPDFToImageLabel', 'Convert PDF to PDF-Image (Used to remove text behind the box)')}
|
||||
checked={parameters.convertPDFToImage}
|
||||
onChange={(e) => onParameterChange('convertPDFToImage', e.currentTarget.checked)}
|
||||
disabled={disabled}
|
||||
size="sm"
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default RedactAdvancedSettings;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RedactMode } from '../../../hooks/tools/redact/useRedactParameters';
|
||||
import ButtonSelector from '../../shared/ButtonSelector';
|
||||
|
||||
interface RedactModeSelectorProps {
|
||||
mode: RedactMode;
|
||||
onModeChange: (mode: RedactMode) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function RedactModeSelector({ mode, onModeChange, disabled }: RedactModeSelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ButtonSelector
|
||||
label={t('redact.modeSelector.mode', 'Mode')}
|
||||
value={mode}
|
||||
onChange={onModeChange}
|
||||
options={[
|
||||
{
|
||||
value: 'automatic' as const,
|
||||
label: t('redact.modeSelector.automatic', 'Automatic'),
|
||||
},
|
||||
{
|
||||
value: 'manual' as const,
|
||||
label: t('redact.modeSelector.manual', 'Manual'),
|
||||
disabled: true, // Keep manual mode disabled until implemented
|
||||
},
|
||||
]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import RedactSingleStepSettings from './RedactSingleStepSettings';
|
||||
import { defaultParameters } from '../../../hooks/tools/redact/useRedactParameters';
|
||||
|
||||
// Mock useTranslation
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: vi.fn((_key: string, fallback: string) => fallback) })
|
||||
}));
|
||||
|
||||
// Wrapper component to provide Mantine context
|
||||
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<MantineProvider>{children}</MantineProvider>
|
||||
);
|
||||
|
||||
describe('RedactSingleStepSettings', () => {
|
||||
const mockOnParameterChange = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render mode selector', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactSingleStepSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Mode')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Automatic' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Manual' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render automatic mode settings when mode is automatic', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactSingleStepSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Default mode is automatic, so these should be visible
|
||||
expect(screen.getByText('Words to Redact')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Enter a word')).toBeInTheDocument();
|
||||
expect(screen.getByText('Box Colour')).toBeInTheDocument();
|
||||
expect(screen.getByText('Use Regex')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render manual mode settings when mode is manual', () => {
|
||||
const manualParameters = {
|
||||
...defaultParameters,
|
||||
mode: 'manual' as const,
|
||||
};
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactSingleStepSettings
|
||||
parameters={manualParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Manual mode should show placeholder text
|
||||
expect(screen.getByText('Manual redaction interface will be available here when implemented.')).toBeInTheDocument();
|
||||
|
||||
// Automatic mode settings should not be visible
|
||||
expect(screen.queryByText('Words to Redact')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should pass through parameter changes from automatic settings', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactSingleStepSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Test adding a word
|
||||
const input = screen.getByPlaceholderText('Enter a word');
|
||||
const addButton = screen.getByRole('button', { name: '+ Add' });
|
||||
|
||||
fireEvent.change(input, { target: { value: 'TestWord' } });
|
||||
fireEvent.click(addButton);
|
||||
|
||||
expect(mockOnParameterChange).toHaveBeenCalledWith('wordsToRedact', ['TestWord']);
|
||||
});
|
||||
|
||||
test('should pass through parameter changes from advanced settings', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactSingleStepSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Test changing color
|
||||
const colorInput = screen.getByDisplayValue('#000000');
|
||||
fireEvent.change(colorInput, { target: { value: '#FF0000' } });
|
||||
|
||||
expect(mockOnParameterChange).toHaveBeenCalledWith('redactColor', '#FF0000');
|
||||
});
|
||||
|
||||
test('should disable all controls when disabled prop is true', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactSingleStepSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
disabled={true}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Mode selector buttons should be disabled
|
||||
expect(screen.getByRole('button', { name: 'Automatic' })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Manual' })).toBeDisabled();
|
||||
|
||||
// Automatic settings controls should be disabled
|
||||
expect(screen.getByPlaceholderText('Enter a word')).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: '+ Add' })).toBeDisabled();
|
||||
expect(screen.getByDisplayValue('#000000')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should show current parameter values in automatic mode', () => {
|
||||
const customParameters = {
|
||||
...defaultParameters,
|
||||
wordsToRedact: ['Word1', 'Word2'],
|
||||
redactColor: '#FF0000',
|
||||
useRegex: true,
|
||||
customPadding: 0.5,
|
||||
};
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactSingleStepSettings
|
||||
parameters={customParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Check that word tags are displayed
|
||||
expect(screen.getByText('Word1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Word2')).toBeInTheDocument();
|
||||
|
||||
// Check that color is displayed
|
||||
expect(screen.getByDisplayValue('#FF0000')).toBeInTheDocument();
|
||||
|
||||
// Check that regex checkbox is checked
|
||||
const useRegexCheckbox = screen.getByLabelText('Use Regex');
|
||||
expect(useRegexCheckbox).toBeChecked();
|
||||
|
||||
// Check that padding value is displayed
|
||||
expect(screen.getByDisplayValue('0.5')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should maintain consistent spacing and layout', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<RedactSingleStepSettings
|
||||
parameters={defaultParameters}
|
||||
onParameterChange={mockOnParameterChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Check that the Stack container exists
|
||||
const container = screen.getByText('Mode').closest('.mantine-Stack-root');
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Stack, Divider } from "@mantine/core";
|
||||
import { RedactParameters } from "../../../hooks/tools/redact/useRedactParameters";
|
||||
import RedactModeSelector from "./RedactModeSelector";
|
||||
import WordsToRedactInput from "./WordsToRedactInput";
|
||||
import RedactAdvancedSettings from "./RedactAdvancedSettings";
|
||||
|
||||
interface RedactSingleStepSettingsProps {
|
||||
parameters: RedactParameters;
|
||||
onParameterChange: <K extends keyof RedactParameters>(key: K, value: RedactParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const RedactSingleStepSettings = ({ parameters, onParameterChange, disabled = false }: RedactSingleStepSettingsProps) => {
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Mode Selection */}
|
||||
<RedactModeSelector
|
||||
mode={parameters.mode}
|
||||
onModeChange={(mode) => onParameterChange('mode', mode)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
{/* Automatic Mode Settings */}
|
||||
{parameters.mode === 'automatic' && (
|
||||
<>
|
||||
<Divider />
|
||||
|
||||
{/* Words to Redact */}
|
||||
<WordsToRedactInput
|
||||
wordsToRedact={parameters.wordsToRedact}
|
||||
onWordsChange={(words) => onParameterChange('wordsToRedact', words)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Advanced Settings */}
|
||||
<RedactAdvancedSettings
|
||||
parameters={parameters}
|
||||
onParameterChange={onParameterChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Manual Mode Placeholder */}
|
||||
{parameters.mode === 'manual' && (
|
||||
<>
|
||||
<Divider />
|
||||
<Stack gap="md">
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
|
||||
Manual redaction interface will be available here when implemented.
|
||||
</div>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default RedactSingleStepSettings;
|
||||
@@ -0,0 +1,191 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import WordsToRedactInput from './WordsToRedactInput';
|
||||
|
||||
// Mock useTranslation
|
||||
const mockT = vi.fn((_key: string, fallback: string) => fallback);
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: mockT })
|
||||
}));
|
||||
|
||||
// Wrapper component to provide Mantine context
|
||||
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<MantineProvider>{children}</MantineProvider>
|
||||
);
|
||||
|
||||
describe('WordsToRedactInput', () => {
|
||||
const mockOnWordsChange = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render with title and input field', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<WordsToRedactInput
|
||||
wordsToRedact={[]}
|
||||
onWordsChange={mockOnWordsChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Words to Redact')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Enter a word')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '+ Add' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ trigger: 'Add button click', action: (_input: HTMLElement, addButton: HTMLElement) => fireEvent.click(addButton) },
|
||||
{ trigger: 'Enter key press', action: (input: HTMLElement) => fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }) },
|
||||
])('should add word when $trigger', ({ action }) => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<WordsToRedactInput
|
||||
wordsToRedact={[]}
|
||||
onWordsChange={mockOnWordsChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter a word');
|
||||
const addButton = screen.getByRole('button', { name: '+ Add' });
|
||||
|
||||
fireEvent.change(input, { target: { value: 'TestWord' } });
|
||||
action(input, addButton);
|
||||
|
||||
expect(mockOnWordsChange).toHaveBeenCalledWith(['TestWord']);
|
||||
});
|
||||
|
||||
test('should not add empty word', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<WordsToRedactInput
|
||||
wordsToRedact={[]}
|
||||
onWordsChange={mockOnWordsChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const addButton = screen.getByRole('button', { name: '+ Add' });
|
||||
|
||||
fireEvent.click(addButton);
|
||||
|
||||
expect(mockOnWordsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not add duplicate word', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<WordsToRedactInput
|
||||
wordsToRedact={['Existing']}
|
||||
onWordsChange={mockOnWordsChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter a word');
|
||||
const addButton = screen.getByRole('button', { name: '+ Add' });
|
||||
|
||||
fireEvent.change(input, { target: { value: 'Existing' } });
|
||||
fireEvent.click(addButton);
|
||||
|
||||
expect(mockOnWordsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should trim whitespace when adding word', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<WordsToRedactInput
|
||||
wordsToRedact={[]}
|
||||
onWordsChange={mockOnWordsChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter a word');
|
||||
const addButton = screen.getByRole('button', { name: '+ Add' });
|
||||
|
||||
fireEvent.change(input, { target: { value: ' TestWord ' } });
|
||||
fireEvent.click(addButton);
|
||||
|
||||
expect(mockOnWordsChange).toHaveBeenCalledWith(['TestWord']);
|
||||
});
|
||||
|
||||
test('should remove word when x button is clicked', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<WordsToRedactInput
|
||||
wordsToRedact={['Word1', 'Word2']}
|
||||
onWordsChange={mockOnWordsChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const removeButtons = screen.getAllByText('×');
|
||||
fireEvent.click(removeButtons[0]);
|
||||
|
||||
expect(mockOnWordsChange).toHaveBeenCalledWith(['Word2']);
|
||||
});
|
||||
|
||||
test('should clear input after adding word', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<WordsToRedactInput
|
||||
wordsToRedact={[]}
|
||||
onWordsChange={mockOnWordsChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter a word') as HTMLInputElement;
|
||||
const addButton = screen.getByRole('button', { name: '+ Add' });
|
||||
|
||||
fireEvent.change(input, { target: { value: 'TestWord' } });
|
||||
fireEvent.click(addButton);
|
||||
|
||||
expect(input.value).toBe('');
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ description: 'disable Add button when input is empty', inputValue: '', expectedDisabled: true },
|
||||
{ description: 'enable Add button when input has text', inputValue: 'TestWord', expectedDisabled: false },
|
||||
])('should $description', ({ inputValue, expectedDisabled }) => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<WordsToRedactInput
|
||||
wordsToRedact={[]}
|
||||
onWordsChange={mockOnWordsChange}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter a word');
|
||||
const addButton = screen.getByRole('button', { name: '+ Add' });
|
||||
|
||||
fireEvent.change(input, { target: { value: inputValue } });
|
||||
|
||||
expect(addButton).toHaveProperty('disabled', expectedDisabled);
|
||||
});
|
||||
|
||||
test('should disable all controls when disabled prop is true', () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<WordsToRedactInput
|
||||
wordsToRedact={['Word1']}
|
||||
onWordsChange={mockOnWordsChange}
|
||||
disabled={true}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter a word');
|
||||
const addButton = screen.getByRole('button', { name: '+ Add' });
|
||||
const removeButton = screen.getByText('×');
|
||||
|
||||
expect(input).toBeDisabled();
|
||||
expect(addButton).toBeDisabled();
|
||||
expect(removeButton.closest('button')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Stack, Text, TextInput, Button, Group, ActionIcon } from '@mantine/core';
|
||||
|
||||
interface WordsToRedactInputProps {
|
||||
wordsToRedact: string[];
|
||||
onWordsChange: (words: string[]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function WordsToRedactInput({ wordsToRedact, onWordsChange, disabled }: WordsToRedactInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const [currentWord, setCurrentWord] = useState('');
|
||||
|
||||
const addWord = () => {
|
||||
if (currentWord.trim() && !wordsToRedact.includes(currentWord.trim())) {
|
||||
onWordsChange([...wordsToRedact, currentWord.trim()]);
|
||||
setCurrentWord('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeWord = (index: number) => {
|
||||
onWordsChange(wordsToRedact.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addWord();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>
|
||||
{t('redact.auto.wordsToRedact.title', 'Words to Redact')}
|
||||
</Text>
|
||||
|
||||
{/* Current words */}
|
||||
{wordsToRedact.map((word, index) => (
|
||||
<Group key={index} justify="space-between" p="sm" style={{
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
border: `1px solid var(--mantine-color-gray-3)`,
|
||||
backgroundColor: 'var(--mantine-color-gray-0)'
|
||||
}}>
|
||||
<Text
|
||||
size="sm"
|
||||
style={{
|
||||
maxWidth: '80%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
title={word}
|
||||
>
|
||||
{word}
|
||||
</Text>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
color="red"
|
||||
onClick={() => removeWord(index)}
|
||||
disabled={disabled}
|
||||
>
|
||||
×
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
))}
|
||||
|
||||
{/* Add new word input */}
|
||||
<Group gap="sm" align="end">
|
||||
<TextInput
|
||||
placeholder={t('redact.auto.wordsToRedact.placeholder', 'Enter a word')}
|
||||
value={currentWord}
|
||||
onChange={(e) => setCurrentWord(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
disabled={disabled}
|
||||
size="sm"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
onClick={addWord}
|
||||
disabled={disabled || !currentWord.trim()}
|
||||
>
|
||||
+ {t('redact.auto.wordsToRedact.add', 'Add')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Examples */}
|
||||
{wordsToRedact.length === 0 && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('redact.auto.wordsToRedact.examples', 'Examples: Confidential, Top-Secret')}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { RemovePasswordParameters } from "../../../hooks/tools/removePassword/us
|
||||
|
||||
interface RemovePasswordSettingsProps {
|
||||
parameters: RemovePasswordParameters;
|
||||
onParameterChange: (key: keyof RemovePasswordParameters, value: string) => void;
|
||||
onParameterChange: <K extends keyof RemovePasswordParameters>(key: K, value: RemovePasswordParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SanitizeParameters, defaultParameters } from "../../../hooks/tools/sani
|
||||
|
||||
interface SanitizeSettingsProps {
|
||||
parameters: SanitizeParameters;
|
||||
onParameterChange: (key: keyof SanitizeParameters, value: boolean) => void;
|
||||
onParameterChange: <K extends keyof SanitizeParameters>(key: K, value: SanitizeParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,12 @@ import { StirlingFile } from "../../../types/fileContext";
|
||||
|
||||
export interface FileStatusIndicatorProps {
|
||||
selectedFiles?: StirlingFile[];
|
||||
placeholder?: string;
|
||||
minFiles?: number;
|
||||
}
|
||||
|
||||
const FileStatusIndicator = ({
|
||||
selectedFiles = [],
|
||||
minFiles = 1,
|
||||
}: FileStatusIndicatorProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { openFilesModal, onFilesSelect } = useFilesModalContext();
|
||||
@@ -55,6 +56,14 @@ const FileStatusIndicator = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const getPlaceholder = () => {
|
||||
if (minFiles === undefined || minFiles === 1) {
|
||||
return t("files.selectFromWorkbench", "Select files from the workbench or ");
|
||||
} else {
|
||||
return t("files.selectMultipleFromWorkbench", "Select at least {{count}} files from the workbench or ", { count: minFiles });
|
||||
}
|
||||
};
|
||||
|
||||
// Check if there are no files in the workbench
|
||||
if (stirlingFileStubs.length === 0) {
|
||||
// If no recent files, show upload button
|
||||
@@ -89,12 +98,12 @@ const FileStatusIndicator = ({
|
||||
}
|
||||
|
||||
// Show selection status when there are files in workbench
|
||||
if (selectedFiles.length === 0) {
|
||||
if (selectedFiles.length < minFiles) {
|
||||
// If no recent files, show upload option
|
||||
if (!hasRecentFiles) {
|
||||
return (
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("files.selectFromWorkbench", "Select files from the workbench or ") + " "}
|
||||
{getPlaceholder() + " "}
|
||||
<Anchor
|
||||
size="sm"
|
||||
onClick={handleNativeUpload}
|
||||
@@ -109,7 +118,7 @@ const FileStatusIndicator = ({
|
||||
// If there are recent files, show add files option
|
||||
return (
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("files.selectFromWorkbench", "Select files from the workbench or ") + " "}
|
||||
{getPlaceholder() + " "}
|
||||
<Anchor
|
||||
size="sm"
|
||||
onClick={() => openFilesModal()}
|
||||
@@ -125,7 +134,7 @@ const FileStatusIndicator = ({
|
||||
|
||||
return (
|
||||
<Text size="sm" c="dimmed" style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||
✓ {selectedFiles.length === 1 ? t("fileSelected", "Selected: {{filename}}", { filename: selectedFiles[0]?.name }) : t("filesSelected", "{{count}} files selected", { count: selectedFiles.length })}
|
||||
✓ {selectedFiles.length === 1 ? t("fileSelected", "Selected: {{filename}}", { filename: selectedFiles[0]?.name }) : t("filesSelected", "{{count}} files selected", { count: selectedFiles.length })}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface FilesToolStepProps {
|
||||
selectedFiles: StirlingFile[];
|
||||
isCollapsed?: boolean;
|
||||
onCollapsedClick?: () => void;
|
||||
placeholder?: string;
|
||||
minFiles?: number;
|
||||
}
|
||||
|
||||
export function createFilesToolStep(
|
||||
@@ -23,7 +23,7 @@ export function createFilesToolStep(
|
||||
}, (
|
||||
<FileStatusIndicator
|
||||
selectedFiles={props.selectedFiles}
|
||||
placeholder={props.placeholder || t("files.placeholder", "Select a PDF file in the main view to get started")}
|
||||
minFiles={props.minFiles}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ const renderTooltipTitle = (
|
||||
<Text fw={400} size="sm">
|
||||
{title}
|
||||
</Text>
|
||||
<LocalIcon icon="gpp-maybe-outline-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--icon-files-color)' }} />
|
||||
<LocalIcon icon="info-outline-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--icon-files-color)' }} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ export function ToolWorkflowTitle({ title, tooltip, description }: ToolWorkflowT
|
||||
<Text fw={500} size="lg" p="xs">
|
||||
{title}
|
||||
</Text>
|
||||
{tooltip && <LocalIcon icon="gpp-maybe-outline-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--icon-files-color)' }} />}
|
||||
{tooltip && <LocalIcon icon="info-outline-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--icon-files-color)' }} />}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { StirlingFile } from '../../../types/fileContext';
|
||||
export interface FilesStepConfig {
|
||||
selectedFiles: StirlingFile[];
|
||||
isCollapsed?: boolean;
|
||||
placeholder?: string;
|
||||
minFiles?: number;
|
||||
onCollapsedClick?: () => void;
|
||||
isVisible?: boolean;
|
||||
}
|
||||
@@ -76,7 +76,7 @@ export function createToolFlow(config: ToolFlowConfig) {
|
||||
{config.files.isVisible !== false && steps.createFilesStep({
|
||||
selectedFiles: config.files.selectedFiles,
|
||||
isCollapsed: config.files.isCollapsed,
|
||||
placeholder: config.files.placeholder,
|
||||
minFiles: config.files.minFiles,
|
||||
onCollapsedClick: config.files.onCollapsedClick
|
||||
})}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Stack, TextInput, Select, Checkbox } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { isSplitMode, SPLIT_MODES, SPLIT_TYPES } from '../../../constants/splitConstants';
|
||||
import { isSplitMethod, SPLIT_METHODS } from '../../../constants/splitConstants';
|
||||
import { SplitParameters } from '../../../hooks/tools/split/useSplitParameters';
|
||||
|
||||
export interface SplitSettingsProps {
|
||||
parameters: SplitParameters;
|
||||
onParameterChange: (parameter: keyof SplitParameters, value: string | boolean) => void;
|
||||
onParameterChange: <K extends keyof SplitParameters>(key: K, value: SplitParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -57,28 +57,37 @@ const SplitSettings = ({
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const renderBySizeOrCountForm = () => (
|
||||
<Stack gap="sm">
|
||||
<Select
|
||||
label={t("split-by-size-or-count.type.label", "Split Type")}
|
||||
value={parameters.splitType}
|
||||
onChange={(v) => v && onParameterChange('splitType', v)}
|
||||
disabled={disabled}
|
||||
data={[
|
||||
{ value: SPLIT_TYPES.SIZE, label: t("split-by-size-or-count.type.size", "By Size") },
|
||||
{ value: SPLIT_TYPES.PAGES, label: t("split-by-size-or-count.type.pageCount", "By Page Count") },
|
||||
{ value: SPLIT_TYPES.DOCS, label: t("split-by-size-or-count.type.docCount", "By Document Count") },
|
||||
]}
|
||||
/>
|
||||
const renderSplitValueForm = () => {
|
||||
let label, placeholder;
|
||||
|
||||
switch (parameters.method) {
|
||||
case SPLIT_METHODS.BY_SIZE:
|
||||
label = t("split.value.fileSize.label", "File Size");
|
||||
placeholder = t("split.value.fileSize.placeholder", "e.g. 10MB, 500KB");
|
||||
break;
|
||||
case SPLIT_METHODS.BY_PAGE_COUNT:
|
||||
label = t("split.value.pageCount.label", "Pages per File");
|
||||
placeholder = t("split.value.pageCount.placeholder", "e.g. 5, 10");
|
||||
break;
|
||||
case SPLIT_METHODS.BY_DOC_COUNT:
|
||||
label = t("split.value.docCount.label", "Number of Files");
|
||||
placeholder = t("split.value.docCount.placeholder", "e.g. 3, 5");
|
||||
break;
|
||||
default:
|
||||
label = t("split-by-size-or-count.value.label", "Split Value");
|
||||
placeholder = t("split-by-size-or-count.value.placeholder", "e.g. 10MB or 5 pages");
|
||||
}
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
label={t("split-by-size-or-count.value.label", "Split Value")}
|
||||
placeholder={t("split-by-size-or-count.value.placeholder", "e.g. 10MB or 5 pages")}
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
value={parameters.splitValue}
|
||||
onChange={(e) => onParameterChange('splitValue', e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
const renderByChaptersForm = () => (
|
||||
<Stack gap="sm">
|
||||
@@ -106,26 +115,30 @@ const SplitSettings = ({
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Mode Selector */}
|
||||
{/* Method Selector */}
|
||||
<Select
|
||||
label="Choose split method"
|
||||
placeholder="Select how to split the PDF"
|
||||
value={parameters.mode}
|
||||
onChange={(v) => isSplitMode(v) && onParameterChange('mode', v)}
|
||||
label={t("split.method.label", "Choose split method")}
|
||||
placeholder={t("split.method.placeholder", "Select how to split the PDF")}
|
||||
value={parameters.method}
|
||||
onChange={(v) => isSplitMethod(v) && onParameterChange('method', v)}
|
||||
disabled={disabled}
|
||||
data={[
|
||||
{ value: SPLIT_MODES.BY_PAGES, label: t("split.header", "Split by Pages") + " (e.g. 1,3,5-10)" },
|
||||
{ value: SPLIT_MODES.BY_SECTIONS, label: t("split-by-sections.title", "Split by Grid Sections") },
|
||||
{ value: SPLIT_MODES.BY_SIZE_OR_COUNT, label: t("split-by-size-or-count.title", "Split by Size or Count") },
|
||||
{ value: SPLIT_MODES.BY_CHAPTERS, label: t("splitByChapters.title", "Split by Chapters") },
|
||||
{ value: SPLIT_METHODS.BY_PAGES, label: t("split.methods.byPages", "Split at Pages Numbers") },
|
||||
{ value: SPLIT_METHODS.BY_SECTIONS, label: t("split.methods.bySections", "Split by Sections") },
|
||||
{ value: SPLIT_METHODS.BY_SIZE, label: t("split.methods.bySize", "Split by Size") },
|
||||
{ value: SPLIT_METHODS.BY_PAGE_COUNT, label: t("split.methods.byPageCount", "Split by Page Count") },
|
||||
{ value: SPLIT_METHODS.BY_DOC_COUNT, label: t("split.methods.byDocCount", "Split by Document Count") },
|
||||
{ value: SPLIT_METHODS.BY_CHAPTERS, label: t("split.methods.byChapters", "Split by Chapters") },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Parameter Form */}
|
||||
{parameters.mode === SPLIT_MODES.BY_PAGES && renderByPagesForm()}
|
||||
{parameters.mode === SPLIT_MODES.BY_SECTIONS && renderBySectionsForm()}
|
||||
{parameters.mode === SPLIT_MODES.BY_SIZE_OR_COUNT && renderBySizeOrCountForm()}
|
||||
{parameters.mode === SPLIT_MODES.BY_CHAPTERS && renderByChaptersForm()}
|
||||
{parameters.method === SPLIT_METHODS.BY_PAGES && renderByPagesForm()}
|
||||
{parameters.method === SPLIT_METHODS.BY_SECTIONS && renderBySectionsForm()}
|
||||
{(parameters.method === SPLIT_METHODS.BY_SIZE ||
|
||||
parameters.method === SPLIT_METHODS.BY_PAGE_COUNT ||
|
||||
parameters.method === SPLIT_METHODS.BY_DOC_COUNT) && renderSplitValueForm()}
|
||||
{parameters.method === SPLIT_METHODS.BY_CHAPTERS && renderByChaptersForm()}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
export const useAdjustPageScaleTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("adjustPageScale.tooltip.header.title", "Page Scale Settings Overview")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("adjustPageScale.tooltip.description.title", "Description"),
|
||||
description: t("adjustPageScale.tooltip.description.text", "Adjust the size of PDF content and change the page dimensions.")
|
||||
},
|
||||
{
|
||||
title: t("adjustPageScale.tooltip.scaleFactor.title", "Scale Factor"),
|
||||
description: t("adjustPageScale.tooltip.scaleFactor.text", "Controls how large or small the content appears on the page. Content is scaled and centered - if scaled content is larger than the page size, it may be cropped."),
|
||||
bullets: [
|
||||
t("adjustPageScale.tooltip.scaleFactor.bullet1", "1.0 = Original size"),
|
||||
t("adjustPageScale.tooltip.scaleFactor.bullet2", "0.5 = Half size (50% smaller)"),
|
||||
t("adjustPageScale.tooltip.scaleFactor.bullet3", "2.0 = Double size (200% larger, may crop)")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("adjustPageScale.tooltip.pageSize.title", "Target Page Size"),
|
||||
description: t("adjustPageScale.tooltip.pageSize.text", "Sets the dimensions of the output PDF pages. 'Keep Original Size' maintains current dimensions, while other options resize to standard paper sizes.")
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
export const useMergeTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
tips: [
|
||||
{
|
||||
title: t('merge.removeDigitalSignature.tooltip.title', 'Remove Digital Signature'),
|
||||
description: t('merge.removeDigitalSignature.tooltip.description', 'Digital signatures will be invalidated when merging files. Check this to remove them from the final merged PDF.')
|
||||
},
|
||||
{
|
||||
title: t('merge.generateTableOfContents.tooltip.title', 'Generate Table of Contents'),
|
||||
description: t('merge.generateTableOfContents.tooltip.description', 'Automatically creates a clickable table of contents in the merged PDF based on the original file names and page numbers.')
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
export const useRedactModeTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("redact.tooltip.mode.header.title", "Redaction Method")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("redact.tooltip.mode.automatic.title", "Automatic Redaction"),
|
||||
description: t("redact.tooltip.mode.automatic.text", "Automatically finds and redacts specified text throughout the document. Perfect for removing consistent sensitive information like names, SSNs, or confidential markers.")
|
||||
},
|
||||
{
|
||||
title: t("redact.tooltip.mode.manual.title", "Manual Redaction"),
|
||||
description: t("redact.tooltip.mode.manual.text", "Click and drag to manually select specific areas to redact. Gives you precise control over what gets redacted. (Coming soon)")
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
export const useRedactWordsTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("redact.tooltip.words.header.title", "Words to Redact")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("redact.tooltip.words.description.title", "Text Matching"),
|
||||
description: t("redact.tooltip.words.description.text", "Enter words or phrases to find and redact in your document. Each word will be searched for separately."),
|
||||
bullets: [
|
||||
t("redact.tooltip.words.bullet1", "Add one word at a time"),
|
||||
t("redact.tooltip.words.bullet2", "Press Enter or click 'Add Another' to add"),
|
||||
t("redact.tooltip.words.bullet3", "Click × to remove words")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("redact.tooltip.words.examples.title", "Common Examples"),
|
||||
description: t("redact.tooltip.words.examples.text", "Typical words to redact include: bank details, email addresses, or specific names.")
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
export const useRedactAdvancedTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("redact.tooltip.advanced.header.title", "Advanced Redaction Settings")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("redact.tooltip.advanced.color.title", "Box Colour & Padding"),
|
||||
description: t("redact.tooltip.advanced.color.text", "Customise the appearance of redaction boxes. Black is standard, but you can choose any colour. Padding adds extra space around the found text."),
|
||||
},
|
||||
{
|
||||
title: t("redact.tooltip.advanced.regex.title", "Use Regex"),
|
||||
description: t("redact.tooltip.advanced.regex.text", "Enable regular expressions for advanced pattern matching. Useful for finding phone numbers, emails, or complex patterns."),
|
||||
bullets: [
|
||||
t("redact.tooltip.advanced.regex.bullet1", "Example: \\d{4}-\\d{2}-\\d{2} to match any dates in YYYY-MM-DD format"),
|
||||
t("redact.tooltip.advanced.regex.bullet2", "Use with caution - test thoroughly")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("redact.tooltip.advanced.wholeWord.title", "Whole Word Search"),
|
||||
description: t("redact.tooltip.advanced.wholeWord.text", "Only match complete words, not partial matches. 'John' won't match 'Johnson' when enabled.")
|
||||
},
|
||||
{
|
||||
title: t("redact.tooltip.advanced.convert.title", "Convert to PDF-Image"),
|
||||
description: t("redact.tooltip.advanced.convert.text", "Converts the PDF to an image-based PDF after redaction. This ensures text behind redaction boxes is completely removed and unrecoverable.")
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
export const useSplitTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("split.tooltip.header.title", "Split Methods Overview")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("split.tooltip.byPages.title", "Split at Page Numbers"),
|
||||
description: t("split.tooltip.byPages.text", "Extract specific pages or ranges from your PDF. Use commas to separate individual pages and hyphens for ranges."),
|
||||
bullets: [
|
||||
t("split.tooltip.byPages.bullet1", "Single pages: 1,3,5"),
|
||||
t("split.tooltip.byPages.bullet2", "Page ranges: 1-5,10-15"),
|
||||
t("split.tooltip.byPages.bullet3", "Mixed: 1,3-7,12,15-20")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("split.tooltip.bySections.title", "Split by Grid Sections"),
|
||||
description: t("split.tooltip.bySections.text", "Divide each page into a grid of sections. Useful for splitting documents with multiple columns or extracting specific areas."),
|
||||
bullets: [
|
||||
t("split.tooltip.bySections.bullet1", "Horizontal: Number of rows to create"),
|
||||
t("split.tooltip.bySections.bullet2", "Vertical: Number of columns to create"),
|
||||
t("split.tooltip.bySections.bullet3", "Merge: Combine all sections into one PDF")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("split.tooltip.bySize.title", "Split by File Size"),
|
||||
description: t("split.tooltip.bySize.text", "Create multiple PDFs that don't exceed a specified file size. Ideal for file size limitations or email attachments."),
|
||||
bullets: [
|
||||
t("split.tooltip.bySize.bullet1", "Use MB for larger files (e.g., 10MB)"),
|
||||
t("split.tooltip.bySize.bullet2", "Use KB for smaller files (e.g., 500KB)"),
|
||||
t("split.tooltip.bySize.bullet3", "System will split at page boundaries")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("split.tooltip.byCount.title", "Split by Count"),
|
||||
description: t("split.tooltip.byCount.text", "Create multiple PDFs with a specific number of pages or documents each."),
|
||||
bullets: [
|
||||
t("split.tooltip.byCount.bullet1", "Page Count: Fixed number of pages per file"),
|
||||
t("split.tooltip.byCount.bullet2", "Document Count: Fixed number of output files"),
|
||||
t("split.tooltip.byCount.bullet3", "Useful for batch processing workflows")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("split.tooltip.byChapters.title", "Split by Chapters"),
|
||||
description: t("split.tooltip.byChapters.text", "Use PDF bookmarks to automatically split at chapter boundaries. Requires PDFs with bookmark structure."),
|
||||
bullets: [
|
||||
t("split.tooltip.byChapters.bullet1", "Bookmark Level: Which level to split on (1=top level)"),
|
||||
t("split.tooltip.byChapters.bullet2", "Include Metadata: Preserve document properties"),
|
||||
t("split.tooltip.byChapters.bullet3", "Allow Duplicates: Handle repeated bookmark names")
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user