Fix/v2/automate_settings_gap_fill (#4574)

All implemented tools now support automation bar Sign. Sign will need
custom automation UI support

---------

Co-authored-by: Connor Yoh <[email protected]>
Co-authored-by: Reece Browne <[email protected]>
This commit is contained in:
ConnorYoh
2025-10-01 23:13:54 +01:00
committed by GitHub
co-authored by Connor Yoh Reece Browne
parent ec05c5c049
commit 510e1c38eb
28 changed files with 1300 additions and 678 deletions
@@ -0,0 +1,119 @@
/**
* AddAttachmentsSettings - Shared settings component for both tool UI and automation
*
* Allows selecting files to attach to PDFs.
*/
import { Stack, Text, Group, ActionIcon, Alert, ScrollArea, Button } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddAttachmentsParameters } from "../../../hooks/tools/addAttachments/useAddAttachmentsParameters";
import LocalIcon from "../../shared/LocalIcon";
interface AddAttachmentsSettingsProps {
parameters: AddAttachmentsParameters;
onParameterChange: <K extends keyof AddAttachmentsParameters>(key: K, value: AddAttachmentsParameters[K]) => void;
disabled?: boolean;
}
const AddAttachmentsSettings = ({ parameters, onParameterChange, disabled = false }: AddAttachmentsSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="md">
<Alert color="blue" variant="light">
<Text size="sm">
{t("AddAttachmentsRequest.info", "Select files to attach to your PDF. These files will be embedded and accessible through the PDF's attachment panel.")}
</Text>
</Alert>
<Stack gap="xs">
<Text size="sm" fw={500}>
{t("AddAttachmentsRequest.selectFiles", "Select Files to Attach")}
</Text>
<input
type="file"
multiple
onChange={(e) => {
const files = Array.from(e.target.files || []);
// Append to existing attachments instead of replacing
const newAttachments = [...(parameters.attachments || []), ...files];
onParameterChange('attachments', newAttachments);
// Reset the input so the same file can be selected again
e.target.value = '';
}}
disabled={disabled}
style={{ display: 'none' }}
id="attachments-input"
/>
<Button
size="xs"
color="blue"
component="label"
htmlFor="attachments-input"
disabled={disabled}
leftSection={<LocalIcon icon="plus" width="14" height="14" />}
>
{parameters.attachments?.length > 0
? t("AddAttachmentsRequest.addMoreFiles", "Add more files...")
: t("AddAttachmentsRequest.placeholder", "Choose files...")
}
</Button>
</Stack>
{parameters.attachments?.length > 0 && (
<Stack gap="xs">
<Text size="sm" fw={500}>
{t("AddAttachmentsRequest.selectedFiles", "Selected Files")} ({parameters.attachments.length})
</Text>
<ScrollArea.Autosize mah={300} type="scroll" offsetScrollbars styles={{ viewport: { overflowX: 'hidden' } }}>
<Stack gap="xs">
{parameters.attachments.map((file, index) => (
<Group key={index} justify="space-between" p="xs" style={{ border: '1px solid var(--mantine-color-gray-3)', borderRadius: 'var(--mantine-radius-sm)', alignItems: 'flex-start' }}>
<Group gap="xs" style={{ flex: 1, minWidth: 0, alignItems: 'flex-start' }}>
{/* Filename (two-line clamp, wraps, no icon on the left) */}
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: 'var(--mantine-font-size-sm)',
fontWeight: 400,
lineHeight: 1.2,
display: '-webkit-box',
WebkitLineClamp: 2 as any,
WebkitBoxOrient: 'vertical' as any,
overflow: 'hidden',
whiteSpace: 'normal',
wordBreak: 'break-word',
}}
title={file.name}
>
{file.name}
</div>
</div>
<Text size="xs" c="dimmed" style={{ flexShrink: 0 }}>
({(file.size / 1024).toFixed(1)} KB)
</Text>
</Group>
<ActionIcon
size="sm"
variant="subtle"
color="red"
style={{ flexShrink: 0 }}
onClick={() => {
const newAttachments = (parameters.attachments || []).filter((_, i) => i !== index);
onParameterChange('attachments', newAttachments);
}}
disabled={disabled}
>
<LocalIcon icon="close-rounded" width="14" height="14" />
</ActionIcon>
</Group>
))}
</Stack>
</ScrollArea.Autosize>
</Stack>
)}
</Stack>
);
};
export default AddAttachmentsSettings;
@@ -0,0 +1,77 @@
/**
* AddPageNumbersAppearanceSettings - Customize Appearance step
*/
import { Stack, Select, TextInput, NumberInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddPageNumbersParameters } from "./useAddPageNumbersParameters";
import { Tooltip } from "../../shared/Tooltip";
interface AddPageNumbersAppearanceSettingsProps {
parameters: AddPageNumbersParameters;
onParameterChange: <K extends keyof AddPageNumbersParameters>(key: K, value: AddPageNumbersParameters[K]) => void;
disabled?: boolean;
}
const AddPageNumbersAppearanceSettings = ({
parameters,
onParameterChange,
disabled = false
}: AddPageNumbersAppearanceSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="md">
<Tooltip content={t('marginTooltip', 'Distance between the page number and the edge of the page.')}>
<Select
label={t('addPageNumbers.selectText.2', 'Margin')}
value={parameters.customMargin}
onChange={(v) => onParameterChange('customMargin', (v as any) || 'medium')}
data={[
{ value: 'small', label: t('sizes.small', 'Small') },
{ value: 'medium', label: t('sizes.medium', 'Medium') },
{ value: 'large', label: t('sizes.large', 'Large') },
{ value: 'x-large', label: t('sizes.x-large', 'Extra Large') },
]}
disabled={disabled}
/>
</Tooltip>
<Tooltip content={t('fontSizeTooltip', 'Size of the page number text in points. Larger numbers create bigger text.')}>
<NumberInput
label={t('addPageNumbers.fontSize', 'Font Size')}
value={parameters.fontSize}
onChange={(v) => onParameterChange('fontSize', typeof v === 'number' ? v : 12)}
min={1}
disabled={disabled}
/>
</Tooltip>
<Tooltip content={t('fontTypeTooltip', 'Font family for the page numbers. Choose based on your document style.')}>
<Select
label={t('addPageNumbers.fontName', 'Font Type')}
value={parameters.fontType}
onChange={(v) => onParameterChange('fontType', (v as any) || 'Times')}
data={[
{ value: 'Times', label: 'Times Roman' },
{ value: 'Helvetica', label: 'Helvetica' },
{ value: 'Courier', label: 'Courier New' },
]}
disabled={disabled}
/>
</Tooltip>
<Tooltip content={t('customTextTooltip', 'Optional custom format for page numbers. Use {n} as placeholder for the number. Example: "Page {n}" will show "Page 1", "Page 2", etc.')}>
<TextInput
label={t('addPageNumbers.selectText.6', 'Custom Text Format')}
value={parameters.customText || ''}
onChange={(e) => onParameterChange('customText', e.currentTarget.value)}
placeholder={t('addPageNumbers.customNumberDesc', 'e.g., "Page {n}" or leave blank for just numbers')}
disabled={disabled}
/>
</Tooltip>
</Stack>
);
};
export default AddPageNumbersAppearanceSettings;
@@ -0,0 +1,55 @@
/**
* AddPageNumbersAutomationSettings - Used for automation only
*
* Combines both position and appearance settings into a single view
*/
import { Stack, Divider, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddPageNumbersParameters } from "./useAddPageNumbersParameters";
import AddPageNumbersPositionSettings from "./AddPageNumbersPositionSettings";
import AddPageNumbersAppearanceSettings from "./AddPageNumbersAppearanceSettings";
interface AddPageNumbersAutomationSettingsProps {
parameters: AddPageNumbersParameters;
onParameterChange: <K extends keyof AddPageNumbersParameters>(key: K, value: AddPageNumbersParameters[K]) => void;
disabled?: boolean;
}
const AddPageNumbersAutomationSettings = ({
parameters,
onParameterChange,
disabled = false
}: AddPageNumbersAutomationSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="lg">
{/* Position & Pages Section */}
<Stack gap="md">
<Text size="sm" fw={600}>{t("addPageNumbers.positionAndPages", "Position & Pages")}</Text>
<AddPageNumbersPositionSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
file={null}
showQuickGrid={true}
/>
</Stack>
<Divider />
{/* Appearance Section */}
<Stack gap="md">
<Text size="sm" fw={600}>{t("addPageNumbers.customize", "Customize Appearance")}</Text>
<AddPageNumbersAppearanceSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
</Stack>
</Stack>
);
};
export default AddPageNumbersAutomationSettings;
@@ -0,0 +1,70 @@
/**
* AddPageNumbersPositionSettings - Position & Pages step
*/
import { Stack, TextInput, NumberInput, Divider, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddPageNumbersParameters } from "./useAddPageNumbersParameters";
import { Tooltip } from "../../shared/Tooltip";
import PageNumberPreview from "./PageNumberPreview";
interface AddPageNumbersPositionSettingsProps {
parameters: AddPageNumbersParameters;
onParameterChange: <K extends keyof AddPageNumbersParameters>(key: K, value: AddPageNumbersParameters[K]) => void;
disabled?: boolean;
file?: File | null;
showQuickGrid?: boolean;
}
const AddPageNumbersPositionSettings = ({
parameters,
onParameterChange,
disabled = false,
file = null,
showQuickGrid = true
}: AddPageNumbersPositionSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="lg">
{/* Position Selection */}
<Stack gap="md">
<PageNumberPreview
parameters={parameters}
onParameterChange={onParameterChange}
file={file}
showQuickGrid={showQuickGrid}
/>
</Stack>
<Divider />
{/* Pages & Starting Number Section */}
<Stack gap="md">
<Text size="sm" fw={500} mb="xs">{t('addPageNumbers.pagesAndStarting', 'Pages & Starting Number')}</Text>
<Tooltip content={t('pageSelectionPrompt', 'Specify which pages to add numbers to. Examples: "1,3,5" for specific pages, "1-5" for ranges, "2n" for even pages, or leave blank for all pages.')}>
<TextInput
label={t('addPageNumbers.selectText.5', 'Pages to Number')}
value={parameters.pagesToNumber || ''}
onChange={(e) => onParameterChange('pagesToNumber', e.currentTarget.value)}
placeholder={t('addPageNumbers.numberPagesDesc', 'e.g., 1,3,5-8 or leave blank for all pages')}
disabled={disabled}
/>
</Tooltip>
<Tooltip content={t('startingNumberTooltip', 'The first number to display. Subsequent pages will increment from this number.')}>
<NumberInput
label={t('addPageNumbers.selectText.4', 'Starting Number')}
value={parameters.startingNumber}
onChange={(v) => onParameterChange('startingNumber', typeof v === 'number' ? v : 1)}
min={1}
disabled={disabled}
/>
</Tooltip>
</Stack>
</Stack>
);
};
export default AddPageNumbersPositionSettings;
@@ -0,0 +1,43 @@
/**
* AddStampAutomationSettings - Used for automation only
*
* This component combines all stamp settings into a single step interface
* for use in the automation system. It includes setup and formatting
* settings in one unified component.
*/
import { Stack } from "@mantine/core";
import { AddStampParameters } from "./useAddStampParameters";
import StampSetupSettings from "./StampSetupSettings";
import StampPositionFormattingSettings from "./StampPositionFormattingSettings";
interface AddStampAutomationSettingsProps {
parameters: AddStampParameters;
onParameterChange: <K extends keyof AddStampParameters>(key: K, value: AddStampParameters[K]) => void;
disabled?: boolean;
}
const AddStampAutomationSettings = ({ parameters, onParameterChange, disabled = false }: AddStampAutomationSettingsProps) => {
return (
<Stack gap="lg">
{/* Stamp Setup (Type, Text/Image, Page Selection) */}
<StampSetupSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
{/* Position and Formatting Settings */}
{parameters.stampType && (
<StampPositionFormattingSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
showPositionGrid={true}
/>
)}
</Stack>
);
};
export default AddStampAutomationSettings;
@@ -0,0 +1,201 @@
import { useTranslation } from "react-i18next";
import { Group, Select, Stack, ColorInput, Button, Slider, Text, NumberInput } from "@mantine/core";
import { AddStampParameters } from "./useAddStampParameters";
import LocalIcon from "../../shared/LocalIcon";
import styles from "./StampPreview.module.css";
import { Tooltip } from "../../shared/Tooltip";
interface StampPositionFormattingSettingsProps {
parameters: AddStampParameters;
onParameterChange: <K extends keyof AddStampParameters>(key: K, value: AddStampParameters[K]) => void;
disabled?: boolean;
showPositionGrid?: boolean; // When true, show the 9-position grid for automation
}
const StampPositionFormattingSettings = ({ parameters, onParameterChange, disabled = false, showPositionGrid = false }: StampPositionFormattingSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="md" justify="space-between">
{/* Position Grid - shown in automation settings */}
{showPositionGrid && (
<Stack gap="xs">
<Text size="sm" fw={500}>{t('AddStampRequest.position', 'Stamp Position')}</Text>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '0.5rem',
maxWidth: '200px'
}}>
{Array.from({ length: 9 }).map((_, i) => {
const idx = (i + 1) as 1|2|3|4|5|6|7|8|9;
const selected = parameters.position === idx;
return (
<Button
key={idx}
variant={selected ? 'filled' : 'outline'}
onClick={() => {
onParameterChange('position', idx);
// Ensure we're using grid positioning, not custom overrides
onParameterChange('overrideX', -1 as any);
onParameterChange('overrideY', -1 as any);
}}
disabled={disabled}
styles={{
root: {
height: '50px',
padding: '0',
}
}}
>
{idx}
</Button>
);
})}
</div>
</Stack>
)}
{/* Icon pill buttons row */}
<div className="flex justify-between gap-[0.5rem]">
<Tooltip content={t('AddStampRequest.rotation', 'Rotation')} position="top">
<Button
variant={parameters._activePill === 'rotation' ? 'filled' : 'outline'}
className="flex-1"
onClick={() => onParameterChange('_activePill', 'rotation')}
>
<LocalIcon icon="rotate-right-rounded" width="1.1rem" height="1.1rem" />
</Button>
</Tooltip>
<Tooltip content={t('AddStampRequest.opacity', 'Opacity')} position="top">
<Button
variant={parameters._activePill === 'opacity' ? 'filled' : 'outline'}
className="flex-1"
onClick={() => onParameterChange('_activePill', 'opacity')}
>
<LocalIcon icon="opacity" width="1.1rem" height="1.1rem" />
</Button>
</Tooltip>
<Tooltip content={parameters.stampType === 'image' ? t('AddStampRequest.imageSize', 'Image Size') : t('AddStampRequest.fontSize', 'Font Size')} position="top">
<Button
variant={parameters._activePill === 'fontSize' ? 'filled' : 'outline'}
className="flex-1"
onClick={() => onParameterChange('_activePill', 'fontSize')}
>
<LocalIcon icon="zoom-in-map-rounded" width="1.1rem" height="1.1rem" />
</Button>
</Tooltip>
</div>
{/* Single slider bound to selected pill */}
{parameters._activePill === 'fontSize' && (
<Stack gap="xs">
<Text className={styles.labelText}>
{parameters.stampType === 'image'
? t('AddStampRequest.imageSize', 'Image Size')
: t('AddStampRequest.fontSize', 'Font Size')
}
</Text>
<Group className={styles.sliderGroup} align="center">
<NumberInput
value={parameters.fontSize}
onChange={(v) => onParameterChange('fontSize', typeof v === 'number' ? v : 1)}
min={1}
max={400}
step={1}
size="sm"
className={styles.numberInput}
disabled={disabled}
/>
<Slider
value={parameters.fontSize}
onChange={(v) => onParameterChange('fontSize', v as number)}
min={1}
max={400}
step={1}
className={styles.slider}
/>
</Group>
</Stack>
)}
{parameters._activePill === 'rotation' && (
<Stack gap="xs">
<Text className={styles.labelText}>{t('AddStampRequest.rotation', 'Rotation')}</Text>
<Group className={styles.sliderGroup} align="center">
<NumberInput
value={parameters.rotation}
onChange={(v) => onParameterChange('rotation', typeof v === 'number' ? v : 0)}
min={-180}
max={180}
step={1}
size="sm"
className={styles.numberInput}
hideControls
disabled={disabled}
/>
<Slider
value={parameters.rotation}
onChange={(v) => onParameterChange('rotation', v as number)}
min={-180}
max={180}
step={1}
className={styles.sliderWide}
/>
</Group>
</Stack>
)}
{parameters._activePill === 'opacity' && (
<Stack gap="xs">
<Text className={styles.labelText}>{t('AddStampRequest.opacity', 'Opacity')}</Text>
<Group className={styles.sliderGroup} align="center">
<NumberInput
value={parameters.opacity}
onChange={(v) => onParameterChange('opacity', typeof v === 'number' ? v : 0)}
min={0}
max={100}
step={1}
size="sm"
className={styles.numberInput}
disabled={disabled}
/>
<Slider
value={parameters.opacity}
onChange={(v) => onParameterChange('opacity', v as number)}
min={0}
max={100}
step={1}
className={styles.slider}
/>
</Group>
</Stack>
)}
{parameters.stampType !== 'image' && (
<ColorInput
label={t('AddStampRequest.customColor', 'Custom Text Color')}
value={parameters.customColor}
onChange={(value) => onParameterChange('customColor', value)}
format="hex"
disabled={disabled}
/>
)}
{/* Margin selection for text stamps */}
{parameters.stampType === 'text' && (
<Select
label={t('AddStampRequest.margin', 'Margin')}
value={parameters.customMargin}
onChange={(v) => onParameterChange('customMargin', (v as any) || 'medium')}
data={[
{ value: 'small', label: t('margin.small', 'Small') },
{ value: 'medium', label: t('margin.medium', 'Medium') },
{ value: 'large', label: t('margin.large', 'Large') },
{ value: 'x-large', label: t('margin.xLarge', 'Extra Large') },
]}
disabled={disabled}
/>
)}
</Stack>
);
};
export default StampPositionFormattingSettings;
@@ -0,0 +1,112 @@
import { useTranslation } from "react-i18next";
import { Stack, Textarea, TextInput, Select, Button, Text, Divider } from "@mantine/core";
import { AddStampParameters } from "./useAddStampParameters";
import ButtonSelector from "../../shared/ButtonSelector";
import styles from "./StampPreview.module.css";
import { getDefaultFontSizeForAlphabet } from "./StampPreviewUtils";
interface StampSetupSettingsProps {
parameters: AddStampParameters;
onParameterChange: <K extends keyof AddStampParameters>(key: K, value: AddStampParameters[K]) => void;
disabled?: boolean;
}
const StampSetupSettings = ({ parameters, onParameterChange, disabled = false }: StampSetupSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="md">
<TextInput
label={t('pageSelectionPrompt', 'Page Selection (e.g. 1,3,2 or 4-8,2,10-12 or 2n-1)')}
value={parameters.pageNumbers}
onChange={(e) => onParameterChange('pageNumbers', e.currentTarget.value)}
disabled={disabled}
/>
<Divider/>
<div>
<Text size="sm" fw={500} mb="xs">{t('AddStampRequest.stampType', 'Stamp Type')}</Text>
<ButtonSelector
value={parameters.stampType}
onChange={(v: 'text' | 'image') => onParameterChange('stampType', v)}
options={[
{ value: 'text', label: t('watermark.type.1', 'Text') },
{ value: 'image', label: t('watermark.type.2', 'Image') },
]}
disabled={disabled}
buttonClassName={styles.modeToggleButton}
textClassName={styles.modeToggleButtonText}
/>
</div>
{parameters.stampType === 'text' && (
<>
<Textarea
label={t('AddStampRequest.stampText', 'Stamp Text')}
value={parameters.stampText}
onChange={(e) => onParameterChange('stampText', e.currentTarget.value)}
autosize
minRows={2}
disabled={disabled}
/>
<Select
label={t('AddStampRequest.alphabet', 'Alphabet')}
value={parameters.alphabet}
onChange={(v) => {
const nextAlphabet = (v as any) || 'roman';
onParameterChange('alphabet', nextAlphabet);
const nextDefault = getDefaultFontSizeForAlphabet(nextAlphabet);
onParameterChange('fontSize', nextDefault);
}}
data={[
{ value: 'roman', label: 'Roman' },
{ value: 'arabic', label: 'العربية' },
{ value: 'japanese', label: '日本語' },
{ value: 'korean', label: '한국어' },
{ value: 'chinese', label: '简体中文' },
{ value: 'thai', label: 'ไทย' },
]}
disabled={disabled}
/>
</>
)}
{parameters.stampType === 'image' && (
<Stack gap="xs">
<input
type="file"
accept=".png,.jpg,.jpeg,.gif,.bmp,.tiff,.tif,.webp"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) onParameterChange('stampImage', file);
}}
disabled={disabled}
style={{ display: 'none' }}
id="stamp-image-input"
/>
<Button
size="xs"
component="label"
htmlFor="stamp-image-input"
disabled={disabled}
>
{t('chooseFile', 'Choose File')}
</Button>
{parameters.stampImage && (
<Stack gap="xs">
<img
src={URL.createObjectURL(parameters.stampImage)}
alt="Selected stamp image"
className="max-h-24 w-full object-contain border border-gray-200 rounded bg-gray-50"
/>
<Text size="xs" c="dimmed">
{parameters.stampImage.name}
</Text>
</Stack>
)}
</Stack>
)}
</Stack>
);
};
export default StampSetupSettings;
@@ -35,7 +35,7 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel,
// Get tool info from registry
const toolInfo = toolRegistry[tool.operation as keyof ToolRegistry];
const SettingsComponent = toolInfo?.settingsComponent;
const SettingsComponent = toolInfo?.automationSettings;
// Initialize parameters from tool (which should contain defaults from registry)
useEffect(() => {
@@ -109,7 +109,7 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel,
{t('automate.config.description', 'Configure the settings for this tool. These settings will be applied when the automation runs.')}
</Text>
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
<div style={{ maxHeight: '60vh', overflowY: 'auto', overflowX: "hidden" }}>
{renderToolSettings()}
</div>
@@ -34,11 +34,14 @@ export default function ToolList({
const handleToolSelect = (index: number, newOperation: string) => {
const defaultParams = getToolDefaultParameters(newOperation);
const toolEntry = toolRegistry[newOperation];
// If tool has no settingsComponent, it's automatically configured
const isConfigured = !toolEntry?.automationSettings;
onToolUpdate(index, {
operation: newOperation,
name: getToolName(newOperation),
configured: false,
configured: isConfigured,
parameters: defaultParams,
});
};
@@ -1,7 +1,7 @@
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Stack, Text, ScrollArea } from '@mantine/core';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
import { ToolRegistryEntry, getToolSupportsAutomate } from '../../../data/toolsTaxonomy';
import { useToolSections } from '../../../hooks/useToolSections';
import { renderToolButtons } from '../shared/renderToolButtons';
import ToolSearch from '../toolPicker/ToolSearch';
@@ -28,9 +28,11 @@ export default function ToolSelector({
const [shouldAutoFocus, setShouldAutoFocus] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Filter out excluded tools (like 'automate' itself)
// Filter out excluded tools (like 'automate' itself) and tools that don't support automation
const baseFilteredTools = useMemo(() => {
return Object.entries(toolRegistry).filter(([key]) => !excludeTools.includes(key));
return Object.entries(toolRegistry).filter(([key, tool]) =>
!excludeTools.includes(key) && getToolSupportsAutomate(tool)
);
}, [toolRegistry, excludeTools]);
// Apply search filter
@@ -0,0 +1,60 @@
/**
* CertSignAutomationSettings - Used for automation only
*
* This component combines all certificate signing settings into a single step interface
* for use in the automation system. It includes sign mode, certificate format, certificate files,
* and signature appearance settings in one unified component.
*/
import { Stack } from "@mantine/core";
import { CertSignParameters } from "../../../hooks/tools/certSign/useCertSignParameters";
import CertificateTypeSettings from "./CertificateTypeSettings";
import CertificateFormatSettings from "./CertificateFormatSettings";
import CertificateFilesSettings from "./CertificateFilesSettings";
import SignatureAppearanceSettings from "./SignatureAppearanceSettings";
interface CertSignAutomationSettingsProps {
parameters: CertSignParameters;
onParameterChange: <K extends keyof CertSignParameters>(key: K, value: CertSignParameters[K]) => void;
disabled?: boolean;
}
const CertSignAutomationSettings = ({ parameters, onParameterChange, disabled = false }: CertSignAutomationSettingsProps) => {
return (
<Stack gap="lg">
{/* Sign Mode Selection (Manual vs Auto) */}
<CertificateTypeSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
{/* Certificate Format - only show for Manual mode */}
{parameters.signMode === 'MANUAL' && (
<CertificateFormatSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
)}
{/* Certificate Files - only show for Manual mode */}
{parameters.signMode === 'MANUAL' && (
<CertificateFilesSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
)}
{/* Signature Appearance Settings */}
<SignatureAppearanceSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
</Stack>
);
};
export default CertSignAutomationSettings;
@@ -0,0 +1,41 @@
/**
* CropAutomationSettings - Used for automation only
*
* Simplified crop settings for automation that doesn't require a file preview.
* Allows users to manually enter crop coordinates and dimensions.
*/
import { Stack } from "@mantine/core";
import { CropParameters } from "../../../hooks/tools/crop/useCropParameters";
import { Rectangle } from "../../../utils/cropCoordinates";
import CropCoordinateInputs from "./CropCoordinateInputs";
interface CropAutomationSettingsProps {
parameters: CropParameters;
onParameterChange: <K extends keyof CropParameters>(key: K, value: CropParameters[K]) => void;
disabled?: boolean;
}
const CropAutomationSettings = ({ parameters, onParameterChange, disabled = false }: CropAutomationSettingsProps) => {
// Handle coordinate changes
const handleCoordinateChange = (field: keyof Rectangle, value: number | string) => {
const numValue = typeof value === 'string' ? parseFloat(value) : value;
if (isNaN(numValue)) return;
const newCropArea = { ...parameters.cropArea, [field]: numValue };
onParameterChange('cropArea', newCropArea);
};
return (
<Stack gap="md">
<CropCoordinateInputs
cropArea={parameters.cropArea}
onCoordinateChange={handleCoordinateChange}
disabled={disabled}
showAutomationInfo={true}
/>
</Stack>
);
};
export default CropAutomationSettings;
@@ -0,0 +1,101 @@
import { Stack, Text, Group, NumberInput, Alert } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { Rectangle, PDFBounds } from "../../../utils/cropCoordinates";
interface CropCoordinateInputsProps {
cropArea: Rectangle;
onCoordinateChange: (field: keyof Rectangle, value: number | string) => void;
disabled?: boolean;
pdfBounds?: PDFBounds;
showAutomationInfo?: boolean;
}
const CropCoordinateInputs = ({
cropArea,
onCoordinateChange,
disabled = false,
pdfBounds,
showAutomationInfo = false
}: CropCoordinateInputsProps) => {
const { t } = useTranslation();
return (
<Stack gap="xs">
{showAutomationInfo && (
<Alert color="blue" variant="light">
<Text size="xs">
{t("crop.automation.info", "Enter crop coordinates in PDF points. Origin (0,0) is at bottom-left. These values will be applied to all PDFs processed in this automation.")}
</Text>
</Alert>
)}
<Text size="sm" fw={500}>
{t("crop.coordinates.title", "Position and Size")}
</Text>
<Group grow>
<NumberInput
label={t("crop.coordinates.x", "X Position")}
description={showAutomationInfo ? t("crop.coordinates.x.desc", "Left edge (points)") : undefined}
value={Math.round(cropArea.x * 10) / 10}
onChange={(value) => onCoordinateChange('x', value)}
disabled={disabled}
min={0}
max={pdfBounds?.actualWidth}
step={0.1}
decimalScale={1}
size={showAutomationInfo ? "sm" : "xs"}
/>
<NumberInput
label={t("crop.coordinates.y", "Y Position")}
description={showAutomationInfo ? t("crop.coordinates.y.desc", "Bottom edge (points)") : undefined}
value={Math.round(cropArea.y * 10) / 10}
onChange={(value) => onCoordinateChange('y', value)}
disabled={disabled}
min={0}
max={pdfBounds?.actualHeight}
step={0.1}
decimalScale={1}
size={showAutomationInfo ? "sm" : "xs"}
/>
</Group>
<Group grow>
<NumberInput
label={t("crop.coordinates.width", "Width")}
description={showAutomationInfo ? t("crop.coordinates.width.desc", "Crop width (points)") : undefined}
value={Math.round(cropArea.width * 10) / 10}
onChange={(value) => onCoordinateChange('width', value)}
disabled={disabled}
min={0.1}
max={pdfBounds?.actualWidth}
step={0.1}
decimalScale={1}
size={showAutomationInfo ? "sm" : "xs"}
/>
<NumberInput
label={t("crop.coordinates.height", "Height")}
description={showAutomationInfo ? t("crop.coordinates.height.desc", "Crop height (points)") : undefined}
value={Math.round(cropArea.height * 10) / 10}
onChange={(value) => onCoordinateChange('height', value)}
disabled={disabled}
min={0.1}
max={pdfBounds?.actualHeight}
step={0.1}
decimalScale={1}
size={showAutomationInfo ? "sm" : "xs"}
/>
</Group>
{showAutomationInfo && (
<Alert color="gray" variant="light">
<Text size="xs">
{t("crop.automation.reference", "Reference: A4 page is 595.28 × 841.89 points (210mm × 297mm). 1 inch = 72 points.")}
</Text>
</Alert>
)}
</Stack>
);
};
export default CropCoordinateInputs;
@@ -1,10 +1,11 @@
import { useMemo, useState, useEffect } from "react";
import { Stack, Text, Box, Group, NumberInput, ActionIcon, Center, Alert } from "@mantine/core";
import { Stack, Text, Box, Group, ActionIcon, Center, Alert } from "@mantine/core";
import { useTranslation } from "react-i18next";
import RestartAltIcon from "@mui/icons-material/RestartAlt";
import { CropParametersHook } from "../../../hooks/tools/crop/useCropParameters";
import { useSelectedFiles } from "../../../contexts/file/fileHooks";
import CropAreaSelector from "./CropAreaSelector";
import CropCoordinateInputs from "./CropCoordinateInputs";
import { DEFAULT_CROP_AREA } from "../../../constants/cropConstants";
import { PAGE_SIZES } from "../../../constants/pageSizeConstants";
import {
@@ -190,71 +191,22 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
</Stack>
{/* Manual Coordinate Input */}
<Stack gap="xs">
<Text size="sm" fw={500}>
{t("crop.coordinates.title", "Position and Size")}
</Text>
<CropCoordinateInputs
cropArea={cropArea}
onCoordinateChange={handleCoordinateChange}
disabled={disabled}
pdfBounds={pdfBounds}
showAutomationInfo={false}
/>
<Group grow>
<NumberInput
label={t("crop.coordinates.x", "X Position")}
value={Math.round(cropArea.x * 10) / 10}
onChange={(value) => handleCoordinateChange('x', value)}
disabled={disabled}
min={0}
max={pdfBounds.actualWidth}
step={0.1}
decimalScale={1}
size="xs"
/>
<NumberInput
label={t("crop.coordinates.y", "Y Position")}
value={Math.round(cropArea.y * 10) / 10}
onChange={(value) => handleCoordinateChange('y', value)}
disabled={disabled}
min={0}
max={pdfBounds.actualHeight}
step={0.1}
decimalScale={1}
size="xs"
/>
</Group>
<Group grow>
<NumberInput
label={t("crop.coordinates.width", "Width")}
value={Math.round(cropArea.width * 10) / 10}
onChange={(value) => handleCoordinateChange('width', value)}
disabled={disabled}
min={0.1}
max={pdfBounds.actualWidth}
step={0.1}
decimalScale={1}
size="xs"
/>
<NumberInput
label={t("crop.coordinates.height", "Height")}
value={Math.round(cropArea.height * 10) / 10}
onChange={(value) => handleCoordinateChange('height', value)}
disabled={disabled}
min={0.1}
max={pdfBounds.actualHeight}
step={0.1}
decimalScale={1}
size="xs"
/>
</Group>
{/* Validation Alert */}
{!isCropValid && (
<Alert color="red" variant="light">
<Text size="xs">
{t("crop.error.invalidArea", "Crop area extends beyond PDF boundaries")}
</Text>
</Alert>
)}
</Stack>
{/* Validation Alert */}
{!isCropValid && (
<Alert color="red" variant="light">
<Text size="xs">
{t("crop.error.invalidArea", "Crop area extends beyond PDF boundaries")}
</Text>
</Alert>
)}
</Stack>
);
};
@@ -16,16 +16,17 @@ const RemovePagesSettings = ({ parameters, onParameterChange, disabled = false }
// Allow user to type naturally - don't normalize input in real-time
onParameterChange('pageNumbers', value);
};
console.log('Current pageNumbers input:', parameters.pageNumbers, disabled);
// Check if current input is valid
const isValid = validatePageNumbers(parameters.pageNumbers);
const hasValue = parameters.pageNumbers.trim().length > 0;
const isValid = validatePageNumbers(parameters.pageNumbers || '');
const hasValue = (parameters?.pageNumbers?.trim().length ?? 0) > 0;
return (
<Stack gap="md">
<TextInput
label={t('removePages.pageNumbers.label', 'Pages to Remove')}
value={parameters.pageNumbers}
value={parameters.pageNumbers || ''}
onChange={(event) => handlePageNumbersChange(event.currentTarget.value)}
placeholder={t('removePages.pageNumbers.placeholder', 'e.g., 1,3,5-8,10')}
disabled={disabled}
@@ -0,0 +1,43 @@
/**
* RotateAutomationSettings - Used for automation only
*
* Simplified rotation settings for automation that allows selecting
* one of four 90-degree rotation angles.
*/
import { Stack, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { RotateParameters } from "../../../hooks/tools/rotate/useRotateParameters";
import ButtonSelector from "../../shared/ButtonSelector";
interface RotateAutomationSettingsProps {
parameters: RotateParameters;
onParameterChange: <K extends keyof RotateParameters>(key: K, value: RotateParameters[K]) => void;
disabled?: boolean;
}
const RotateAutomationSettings = ({ parameters, onParameterChange, disabled = false }: RotateAutomationSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="md">
<Text size="sm" fw={500}>
{t("rotate.selectRotation", "Select Rotation Angle (Clockwise)")}
</Text>
<ButtonSelector
value={parameters.angle}
onChange={(value: number) => onParameterChange('angle', value)}
options={[
{ value: 0, label: "0°" },
{ value: 90, label: "90°" },
{ value: 180, label: "180°" },
{ value: 270, label: "270°" },
]}
disabled={disabled}
/>
</Stack>
);
};
export default RotateAutomationSettings;
@@ -0,0 +1,62 @@
/**
* SplitAutomationSettings - Used for automation only
*
* Combines split method selection and method-specific settings
* into a single component for automation workflows.
*/
import { Stack, Text, Select } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { SplitParameters } from "../../../hooks/tools/split/useSplitParameters";
import { METHOD_OPTIONS, SplitMethod } from "../../../constants/splitConstants";
import SplitSettings from "./SplitSettings";
interface SplitAutomationSettingsProps {
parameters: SplitParameters;
onParameterChange: <K extends keyof SplitParameters>(key: K, value: SplitParameters[K]) => void;
disabled?: boolean;
}
const SplitAutomationSettings = ({ parameters, onParameterChange, disabled = false }: SplitAutomationSettingsProps) => {
const { t } = useTranslation();
// Convert METHOD_OPTIONS to Select data format
const methodSelectOptions = METHOD_OPTIONS.map((option) => {
const prefix = t(option.prefixKey, "Split");
const name = t(option.nameKey, "Method");
return {
value: option.value,
label: `${prefix} ${name}`,
};
});
return (
<Stack gap="lg">
{/* Method Selection */}
<Select
label={t("split.steps.chooseMethod", "Choose Method")}
placeholder={t("split.selectMethod", "Select a split method")}
value={parameters.method}
onChange={(value) => onParameterChange('method', value as (SplitMethod | '') || '')}
data={methodSelectOptions}
disabled={disabled}
/>
{/* Method-Specific Settings */}
{parameters.method && (
<>
<Text size="sm" fw={500}>
{t("split.steps.settings", "Settings")}
</Text>
<SplitSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
</>
)}
</Stack>
);
};
export default SplitAutomationSettings;