mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
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 <connor@stirlingpdf.com> Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
This commit is contained in:
parent
ec05c5c049
commit
510e1c38eb
@ -884,6 +884,7 @@
|
|||||||
"rotate": {
|
"rotate": {
|
||||||
"title": "Rotate PDF",
|
"title": "Rotate PDF",
|
||||||
"submit": "Apply Rotation",
|
"submit": "Apply Rotation",
|
||||||
|
"selectRotation": "Select Rotation Angle (Clockwise)",
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "An error occurred while rotating the PDF."
|
"failed": "An error occurred while rotating the PDF."
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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;
|
||||||
112
frontend/src/components/tools/addStamp/StampSetupSettings.tsx
Normal file
112
frontend/src/components/tools/addStamp/StampSetupSettings.tsx
Normal file
@ -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
|
// Get tool info from registry
|
||||||
const toolInfo = toolRegistry[tool.operation as keyof ToolRegistry];
|
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)
|
// Initialize parameters from tool (which should contain defaults from registry)
|
||||||
useEffect(() => {
|
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.')}
|
{t('automate.config.description', 'Configure the settings for this tool. These settings will be applied when the automation runs.')}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
<div style={{ maxHeight: '60vh', overflowY: 'auto', overflowX: "hidden" }}>
|
||||||
{renderToolSettings()}
|
{renderToolSettings()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -34,11 +34,14 @@ export default function ToolList({
|
|||||||
|
|
||||||
const handleToolSelect = (index: number, newOperation: string) => {
|
const handleToolSelect = (index: number, newOperation: string) => {
|
||||||
const defaultParams = getToolDefaultParameters(newOperation);
|
const defaultParams = getToolDefaultParameters(newOperation);
|
||||||
|
const toolEntry = toolRegistry[newOperation];
|
||||||
|
// If tool has no settingsComponent, it's automatically configured
|
||||||
|
const isConfigured = !toolEntry?.automationSettings;
|
||||||
|
|
||||||
onToolUpdate(index, {
|
onToolUpdate(index, {
|
||||||
operation: newOperation,
|
operation: newOperation,
|
||||||
name: getToolName(newOperation),
|
name: getToolName(newOperation),
|
||||||
configured: false,
|
configured: isConfigured,
|
||||||
parameters: defaultParams,
|
parameters: defaultParams,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Stack, Text, ScrollArea } from '@mantine/core';
|
import { Stack, Text, ScrollArea } from '@mantine/core';
|
||||||
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
import { ToolRegistryEntry, getToolSupportsAutomate } from '../../../data/toolsTaxonomy';
|
||||||
import { useToolSections } from '../../../hooks/useToolSections';
|
import { useToolSections } from '../../../hooks/useToolSections';
|
||||||
import { renderToolButtons } from '../shared/renderToolButtons';
|
import { renderToolButtons } from '../shared/renderToolButtons';
|
||||||
import ToolSearch from '../toolPicker/ToolSearch';
|
import ToolSearch from '../toolPicker/ToolSearch';
|
||||||
@ -28,9 +28,11 @@ export default function ToolSelector({
|
|||||||
const [shouldAutoFocus, setShouldAutoFocus] = useState(false);
|
const [shouldAutoFocus, setShouldAutoFocus] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
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(() => {
|
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]);
|
}, [toolRegistry, excludeTools]);
|
||||||
|
|
||||||
// Apply search filter
|
// 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;
|
||||||
101
frontend/src/components/tools/crop/CropCoordinateInputs.tsx
Normal file
101
frontend/src/components/tools/crop/CropCoordinateInputs.tsx
Normal file
@ -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 { 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 { useTranslation } from "react-i18next";
|
||||||
import RestartAltIcon from "@mui/icons-material/RestartAlt";
|
import RestartAltIcon from "@mui/icons-material/RestartAlt";
|
||||||
import { CropParametersHook } from "../../../hooks/tools/crop/useCropParameters";
|
import { CropParametersHook } from "../../../hooks/tools/crop/useCropParameters";
|
||||||
import { useSelectedFiles } from "../../../contexts/file/fileHooks";
|
import { useSelectedFiles } from "../../../contexts/file/fileHooks";
|
||||||
import CropAreaSelector from "./CropAreaSelector";
|
import CropAreaSelector from "./CropAreaSelector";
|
||||||
|
import CropCoordinateInputs from "./CropCoordinateInputs";
|
||||||
import { DEFAULT_CROP_AREA } from "../../../constants/cropConstants";
|
import { DEFAULT_CROP_AREA } from "../../../constants/cropConstants";
|
||||||
import { PAGE_SIZES } from "../../../constants/pageSizeConstants";
|
import { PAGE_SIZES } from "../../../constants/pageSizeConstants";
|
||||||
import {
|
import {
|
||||||
@ -190,61 +191,13 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{/* Manual Coordinate Input */}
|
{/* Manual Coordinate Input */}
|
||||||
<Stack gap="xs">
|
<CropCoordinateInputs
|
||||||
<Text size="sm" fw={500}>
|
cropArea={cropArea}
|
||||||
{t("crop.coordinates.title", "Position and Size")}
|
onCoordinateChange={handleCoordinateChange}
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Group grow>
|
|
||||||
<NumberInput
|
|
||||||
label={t("crop.coordinates.x", "X Position")}
|
|
||||||
value={Math.round(cropArea.x * 10) / 10}
|
|
||||||
onChange={(value) => handleCoordinateChange('x', value)}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
min={0}
|
pdfBounds={pdfBounds}
|
||||||
max={pdfBounds.actualWidth}
|
showAutomationInfo={false}
|
||||||
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 */}
|
{/* Validation Alert */}
|
||||||
{!isCropValid && (
|
{!isCropValid && (
|
||||||
@ -255,7 +208,6 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -16,16 +16,17 @@ const RemovePagesSettings = ({ parameters, onParameterChange, disabled = false }
|
|||||||
// Allow user to type naturally - don't normalize input in real-time
|
// Allow user to type naturally - don't normalize input in real-time
|
||||||
onParameterChange('pageNumbers', value);
|
onParameterChange('pageNumbers', value);
|
||||||
};
|
};
|
||||||
|
console.log('Current pageNumbers input:', parameters.pageNumbers, disabled);
|
||||||
|
|
||||||
// Check if current input is valid
|
// Check if current input is valid
|
||||||
const isValid = validatePageNumbers(parameters.pageNumbers);
|
const isValid = validatePageNumbers(parameters.pageNumbers || '');
|
||||||
const hasValue = parameters.pageNumbers.trim().length > 0;
|
const hasValue = (parameters?.pageNumbers?.trim().length ?? 0) > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('removePages.pageNumbers.label', 'Pages to Remove')}
|
label={t('removePages.pageNumbers.label', 'Pages to Remove')}
|
||||||
value={parameters.pageNumbers}
|
value={parameters.pageNumbers || ''}
|
||||||
onChange={(event) => handlePageNumbersChange(event.currentTarget.value)}
|
onChange={(event) => handlePageNumbersChange(event.currentTarget.value)}
|
||||||
placeholder={t('removePages.pageNumbers.placeholder', 'e.g., 1,3,5-8,10')}
|
placeholder={t('removePages.pageNumbers.placeholder', 'e.g., 1,3,5-8,10')}
|
||||||
disabled={disabled}
|
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;
|
||||||
@ -37,14 +37,14 @@ export type ToolRegistryEntry = {
|
|||||||
endpoints?: string[];
|
endpoints?: string[];
|
||||||
link?: string;
|
link?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
// URL path for routing (e.g., '/split-pdfs', '/compress-pdf')
|
|
||||||
urlPath?: string;
|
|
||||||
// Workbench type for navigation
|
// Workbench type for navigation
|
||||||
workbench?: WorkbenchType;
|
workbench?: WorkbenchType;
|
||||||
// Operation configuration for automation
|
// Operation configuration for automation
|
||||||
operationConfig?: ToolOperationConfig<any>;
|
operationConfig?: ToolOperationConfig<any>;
|
||||||
// Settings component for automation configuration
|
// Settings component for automation configuration
|
||||||
settingsComponent?: React.ComponentType<any>;
|
automationSettings: React.ComponentType<any> | null;
|
||||||
|
// Whether this tool supports automation (defaults to true)
|
||||||
|
supportsAutomate?: boolean;
|
||||||
// Synonyms for search (optional)
|
// Synonyms for search (optional)
|
||||||
synonyms?: string[];
|
synonyms?: string[];
|
||||||
}
|
}
|
||||||
@ -130,8 +130,8 @@ export const getToolWorkbench = (tool: ToolRegistryEntry): WorkbenchType => {
|
|||||||
/**
|
/**
|
||||||
* Get URL path for a tool
|
* Get URL path for a tool
|
||||||
*/
|
*/
|
||||||
export const getToolUrlPath = (toolId: string, tool: ToolRegistryEntry): string => {
|
export const getToolUrlPath = (toolId: string): string => {
|
||||||
return tool.urlPath || `/${toolId.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
|
return `/${toolId.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -140,3 +140,10 @@ export const getToolUrlPath = (toolId: string, tool: ToolRegistryEntry): string
|
|||||||
export const isValidToolId = (toolId: string, registry: ToolRegistry): boolean => {
|
export const isValidToolId = (toolId: string, registry: ToolRegistry): boolean => {
|
||||||
return toolId in registry;
|
return toolId in registry;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a tool supports automation (defaults to true)
|
||||||
|
*/
|
||||||
|
export const getToolSupportsAutomate = (tool: ToolRegistryEntry): boolean => {
|
||||||
|
return tool.supportsAutomate !== false;
|
||||||
|
};
|
||||||
|
|||||||
@ -61,22 +61,19 @@ import { cropOperationConfig } from "../hooks/tools/crop/useCropOperation";
|
|||||||
import { removeAnnotationsOperationConfig } from "../hooks/tools/removeAnnotations/useRemoveAnnotationsOperation";
|
import { removeAnnotationsOperationConfig } from "../hooks/tools/removeAnnotations/useRemoveAnnotationsOperation";
|
||||||
import { extractImagesOperationConfig } from "../hooks/tools/extractImages/useExtractImagesOperation";
|
import { extractImagesOperationConfig } from "../hooks/tools/extractImages/useExtractImagesOperation";
|
||||||
import { replaceColorOperationConfig } from "../hooks/tools/replaceColor/useReplaceColorOperation";
|
import { replaceColorOperationConfig } from "../hooks/tools/replaceColor/useReplaceColorOperation";
|
||||||
|
import { removePagesOperationConfig } from "../hooks/tools/removePages/useRemovePagesOperation";
|
||||||
|
import { removeBlanksOperationConfig } from "../hooks/tools/removeBlanks/useRemoveBlanksOperation";
|
||||||
import CompressSettings from "../components/tools/compress/CompressSettings";
|
import CompressSettings from "../components/tools/compress/CompressSettings";
|
||||||
import SplitSettings from "../components/tools/split/SplitSettings";
|
|
||||||
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
||||||
import RemovePasswordSettings from "../components/tools/removePassword/RemovePasswordSettings";
|
import RemovePasswordSettings from "../components/tools/removePassword/RemovePasswordSettings";
|
||||||
import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings";
|
import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings";
|
||||||
import RepairSettings from "../components/tools/repair/RepairSettings";
|
|
||||||
import UnlockPdfFormsSettings from "../components/tools/unlockPdfForms/UnlockPdfFormsSettings";
|
|
||||||
import AddWatermarkSingleStepSettings from "../components/tools/addWatermark/AddWatermarkSingleStepSettings";
|
import AddWatermarkSingleStepSettings from "../components/tools/addWatermark/AddWatermarkSingleStepSettings";
|
||||||
import OCRSettings from "../components/tools/ocr/OCRSettings";
|
import OCRSettings from "../components/tools/ocr/OCRSettings";
|
||||||
import ConvertSettings from "../components/tools/convert/ConvertSettings";
|
import ConvertSettings from "../components/tools/convert/ConvertSettings";
|
||||||
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
|
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
|
||||||
import CertificateTypeSettings from "../components/tools/certSign/CertificateTypeSettings";
|
|
||||||
import BookletImpositionSettings from "../components/tools/bookletImposition/BookletImpositionSettings";
|
import BookletImpositionSettings from "../components/tools/bookletImposition/BookletImpositionSettings";
|
||||||
import FlattenSettings from "../components/tools/flatten/FlattenSettings";
|
import FlattenSettings from "../components/tools/flatten/FlattenSettings";
|
||||||
import RedactSingleStepSettings from "../components/tools/redact/RedactSingleStepSettings";
|
import RedactSingleStepSettings from "../components/tools/redact/RedactSingleStepSettings";
|
||||||
import RotateSettings from "../components/tools/rotate/RotateSettings";
|
|
||||||
import Redact from "../tools/Redact";
|
import Redact from "../tools/Redact";
|
||||||
import AdjustPageScale from "../tools/AdjustPageScale";
|
import AdjustPageScale from "../tools/AdjustPageScale";
|
||||||
import ReplaceColor from "../tools/ReplaceColor";
|
import ReplaceColor from "../tools/ReplaceColor";
|
||||||
@ -89,15 +86,22 @@ import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustP
|
|||||||
import ScannerImageSplitSettings from "../components/tools/scannerImageSplit/ScannerImageSplitSettings";
|
import ScannerImageSplitSettings from "../components/tools/scannerImageSplit/ScannerImageSplitSettings";
|
||||||
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
|
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
|
||||||
import SignSettings from "../components/tools/sign/SignSettings";
|
import SignSettings from "../components/tools/sign/SignSettings";
|
||||||
import CropSettings from "../components/tools/crop/CropSettings";
|
|
||||||
import AddPageNumbers from "../tools/AddPageNumbers";
|
import AddPageNumbers from "../tools/AddPageNumbers";
|
||||||
import { addPageNumbersOperationConfig } from "../components/tools/addPageNumbers/useAddPageNumbersOperation";
|
import { addPageNumbersOperationConfig } from "../components/tools/addPageNumbers/useAddPageNumbersOperation";
|
||||||
import RemoveAnnotations from "../tools/RemoveAnnotations";
|
import RemoveAnnotations from "../tools/RemoveAnnotations";
|
||||||
import RemoveAnnotationsSettings from "../components/tools/removeAnnotations/RemoveAnnotationsSettings";
|
|
||||||
import PageLayoutSettings from "../components/tools/pageLayout/PageLayoutSettings";
|
import PageLayoutSettings from "../components/tools/pageLayout/PageLayoutSettings";
|
||||||
import ExtractImages from "../tools/ExtractImages";
|
import ExtractImages from "../tools/ExtractImages";
|
||||||
import ExtractImagesSettings from "../components/tools/extractImages/ExtractImagesSettings";
|
import ExtractImagesSettings from "../components/tools/extractImages/ExtractImagesSettings";
|
||||||
import ReplaceColorSettings from "../components/tools/replaceColor/ReplaceColorSettings";
|
import ReplaceColorSettings from "../components/tools/replaceColor/ReplaceColorSettings";
|
||||||
|
import AddStampAutomationSettings from "../components/tools/addStamp/AddStampAutomationSettings";
|
||||||
|
import CertSignAutomationSettings from "../components/tools/certSign/CertSignAutomationSettings";
|
||||||
|
import CropAutomationSettings from "../components/tools/crop/CropAutomationSettings";
|
||||||
|
import RotateAutomationSettings from "../components/tools/rotate/RotateAutomationSettings";
|
||||||
|
import SplitAutomationSettings from "../components/tools/split/SplitAutomationSettings";
|
||||||
|
import AddAttachmentsSettings from "../components/tools/addAttachments/AddAttachmentsSettings";
|
||||||
|
import RemovePagesSettings from "../components/tools/removePages/RemovePagesSettings";
|
||||||
|
import RemoveBlanksSettings from "../components/tools/removeBlanks/RemoveBlanksSettings";
|
||||||
|
import AddPageNumbersAutomationSettings from "../components/tools/addPageNumbers/AddPageNumbersAutomationSettings";
|
||||||
|
|
||||||
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
||||||
|
|
||||||
@ -199,6 +203,8 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
subcategoryId: SubcategoryId.GENERAL,
|
subcategoryId: SubcategoryId.GENERAL,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
synonyms: getSynonyms(t, "multiTool"),
|
synonyms: getSynonyms(t, "multiTool"),
|
||||||
|
supportsAutomate: false,
|
||||||
|
automationSettings: null
|
||||||
},
|
},
|
||||||
merge: {
|
merge: {
|
||||||
icon: <LocalIcon icon="library-add-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="library-add-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -210,7 +216,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["merge-pdfs"],
|
endpoints: ["merge-pdfs"],
|
||||||
operationConfig: mergeOperationConfig,
|
operationConfig: mergeOperationConfig,
|
||||||
settingsComponent: MergeSettings,
|
automationSettings: MergeSettings,
|
||||||
synonyms: getSynonyms(t, "merge")
|
synonyms: getSynonyms(t, "merge")
|
||||||
},
|
},
|
||||||
// Signing
|
// Signing
|
||||||
@ -225,7 +231,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["cert-sign"],
|
endpoints: ["cert-sign"],
|
||||||
operationConfig: certSignOperationConfig,
|
operationConfig: certSignOperationConfig,
|
||||||
settingsComponent: CertificateTypeSettings,
|
automationSettings: CertSignAutomationSettings,
|
||||||
},
|
},
|
||||||
sign: {
|
sign: {
|
||||||
icon: <LocalIcon icon="signature-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="signature-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -235,8 +241,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.SIGNING,
|
subcategoryId: SubcategoryId.SIGNING,
|
||||||
operationConfig: signOperationConfig,
|
operationConfig: signOperationConfig,
|
||||||
settingsComponent: SignSettings,
|
automationSettings: SignSettings, // TODO:: not all settings shown, suggested next tools shown
|
||||||
synonyms: getSynonyms(t, "sign")
|
synonyms: getSynonyms(t, "sign"),
|
||||||
|
supportsAutomate: false, //TODO make support Sign
|
||||||
},
|
},
|
||||||
|
|
||||||
// Document Security
|
// Document Security
|
||||||
@ -251,7 +258,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["add-password"],
|
endpoints: ["add-password"],
|
||||||
operationConfig: addPasswordOperationConfig,
|
operationConfig: addPasswordOperationConfig,
|
||||||
settingsComponent: AddPasswordSettings,
|
automationSettings: AddPasswordSettings,
|
||||||
synonyms: getSynonyms(t, "addPassword")
|
synonyms: getSynonyms(t, "addPassword")
|
||||||
},
|
},
|
||||||
watermark: {
|
watermark: {
|
||||||
@ -264,7 +271,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
||||||
endpoints: ["add-watermark"],
|
endpoints: ["add-watermark"],
|
||||||
operationConfig: addWatermarkOperationConfig,
|
operationConfig: addWatermarkOperationConfig,
|
||||||
settingsComponent: AddWatermarkSingleStepSettings,
|
automationSettings: AddWatermarkSingleStepSettings,
|
||||||
synonyms: getSynonyms(t, "watermark")
|
synonyms: getSynonyms(t, "watermark")
|
||||||
},
|
},
|
||||||
addStamp: {
|
addStamp: {
|
||||||
@ -278,6 +285,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["add-stamp"],
|
endpoints: ["add-stamp"],
|
||||||
operationConfig: addStampOperationConfig,
|
operationConfig: addStampOperationConfig,
|
||||||
|
automationSettings: AddStampAutomationSettings,
|
||||||
},
|
},
|
||||||
sanitize: {
|
sanitize: {
|
||||||
icon: <LocalIcon icon="cleaning-services-outline-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="cleaning-services-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -289,7 +297,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"),
|
description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"),
|
||||||
endpoints: ["sanitize-pdf"],
|
endpoints: ["sanitize-pdf"],
|
||||||
operationConfig: sanitizeOperationConfig,
|
operationConfig: sanitizeOperationConfig,
|
||||||
settingsComponent: SanitizeSettings,
|
automationSettings: SanitizeSettings,
|
||||||
synonyms: getSynonyms(t, "sanitize")
|
synonyms: getSynonyms(t, "sanitize")
|
||||||
},
|
},
|
||||||
flatten: {
|
flatten: {
|
||||||
@ -302,7 +310,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["flatten"],
|
endpoints: ["flatten"],
|
||||||
operationConfig: flattenOperationConfig,
|
operationConfig: flattenOperationConfig,
|
||||||
settingsComponent: FlattenSettings,
|
automationSettings: FlattenSettings,
|
||||||
synonyms: getSynonyms(t, "flatten")
|
synonyms: getSynonyms(t, "flatten")
|
||||||
},
|
},
|
||||||
unlockPDFForms: {
|
unlockPDFForms: {
|
||||||
@ -315,8 +323,8 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["unlock-pdf-forms"],
|
endpoints: ["unlock-pdf-forms"],
|
||||||
operationConfig: unlockPdfFormsOperationConfig,
|
operationConfig: unlockPdfFormsOperationConfig,
|
||||||
settingsComponent: UnlockPdfFormsSettings,
|
|
||||||
synonyms: getSynonyms(t, "unlockPDFForms"),
|
synonyms: getSynonyms(t, "unlockPDFForms"),
|
||||||
|
automationSettings: null
|
||||||
},
|
},
|
||||||
changePermissions: {
|
changePermissions: {
|
||||||
icon: <LocalIcon icon="lock-outline" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="lock-outline" width="1.5rem" height="1.5rem" />,
|
||||||
@ -328,7 +336,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["add-password"],
|
endpoints: ["add-password"],
|
||||||
operationConfig: changePermissionsOperationConfig,
|
operationConfig: changePermissionsOperationConfig,
|
||||||
settingsComponent: ChangePermissionsSettings,
|
automationSettings: ChangePermissionsSettings,
|
||||||
synonyms: getSynonyms(t, "changePermissions"),
|
synonyms: getSynonyms(t, "changePermissions"),
|
||||||
},
|
},
|
||||||
getPdfInfo: {
|
getPdfInfo: {
|
||||||
@ -339,6 +347,8 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.VERIFICATION,
|
subcategoryId: SubcategoryId.VERIFICATION,
|
||||||
synonyms: getSynonyms(t, "getPdfInfo"),
|
synonyms: getSynonyms(t, "getPdfInfo"),
|
||||||
|
supportsAutomate: false,
|
||||||
|
automationSettings: null
|
||||||
},
|
},
|
||||||
validateSignature: {
|
validateSignature: {
|
||||||
icon: <LocalIcon icon="verified-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="verified-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -348,6 +358,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.VERIFICATION,
|
subcategoryId: SubcategoryId.VERIFICATION,
|
||||||
synonyms: getSynonyms(t, "validateSignature"),
|
synonyms: getSynonyms(t, "validateSignature"),
|
||||||
|
automationSettings: null
|
||||||
},
|
},
|
||||||
|
|
||||||
// Document Review
|
// Document Review
|
||||||
@ -363,7 +374,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
),
|
),
|
||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.DOCUMENT_REVIEW,
|
subcategoryId: SubcategoryId.DOCUMENT_REVIEW,
|
||||||
synonyms: getSynonyms(t, "read")
|
synonyms: getSynonyms(t, "read"),
|
||||||
|
supportsAutomate: false,
|
||||||
|
automationSettings: null
|
||||||
},
|
},
|
||||||
changeMetadata: {
|
changeMetadata: {
|
||||||
icon: <LocalIcon icon="assignment-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="assignment-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -375,7 +388,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["update-metadata"],
|
endpoints: ["update-metadata"],
|
||||||
operationConfig: changeMetadataOperationConfig,
|
operationConfig: changeMetadataOperationConfig,
|
||||||
settingsComponent: ChangeMetadataSingleStep,
|
automationSettings: ChangeMetadataSingleStep,
|
||||||
synonyms: getSynonyms(t, "changeMetadata")
|
synonyms: getSynonyms(t, "changeMetadata")
|
||||||
},
|
},
|
||||||
// Page Formatting
|
// Page Formatting
|
||||||
@ -390,7 +403,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["crop"],
|
endpoints: ["crop"],
|
||||||
operationConfig: cropOperationConfig,
|
operationConfig: cropOperationConfig,
|
||||||
settingsComponent: CropSettings,
|
automationSettings: CropAutomationSettings,
|
||||||
},
|
},
|
||||||
rotate: {
|
rotate: {
|
||||||
icon: <LocalIcon icon="rotate-right-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="rotate-right-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -402,7 +415,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["rotate-pdf"],
|
endpoints: ["rotate-pdf"],
|
||||||
operationConfig: rotateOperationConfig,
|
operationConfig: rotateOperationConfig,
|
||||||
settingsComponent: RotateSettings,
|
automationSettings: RotateAutomationSettings,
|
||||||
synonyms: getSynonyms(t, "rotate")
|
synonyms: getSynonyms(t, "rotate")
|
||||||
},
|
},
|
||||||
split: {
|
split: {
|
||||||
@ -413,7 +426,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||||
operationConfig: splitOperationConfig,
|
operationConfig: splitOperationConfig,
|
||||||
settingsComponent: SplitSettings,
|
automationSettings: SplitAutomationSettings,
|
||||||
synonyms: getSynonyms(t, "split")
|
synonyms: getSynonyms(t, "split")
|
||||||
},
|
},
|
||||||
reorganizePages: {
|
reorganizePages: {
|
||||||
@ -428,7 +441,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||||
endpoints: ["rearrange-pages"],
|
endpoints: ["rearrange-pages"],
|
||||||
operationConfig: reorganizePagesOperationConfig,
|
operationConfig: reorganizePagesOperationConfig,
|
||||||
synonyms: getSynonyms(t, "reorganizePages")
|
synonyms: getSynonyms(t, "reorganizePages"),
|
||||||
|
automationSettings: null
|
||||||
|
|
||||||
},
|
},
|
||||||
scalePages: {
|
scalePages: {
|
||||||
icon: <LocalIcon icon="crop-free-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="crop-free-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -440,7 +455,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["scale-pages"],
|
endpoints: ["scale-pages"],
|
||||||
operationConfig: adjustPageScaleOperationConfig,
|
operationConfig: adjustPageScaleOperationConfig,
|
||||||
settingsComponent: AdjustPageScaleSettings,
|
automationSettings: AdjustPageScaleSettings,
|
||||||
synonyms: getSynonyms(t, "scalePages")
|
synonyms: getSynonyms(t, "scalePages")
|
||||||
},
|
},
|
||||||
addPageNumbers: {
|
addPageNumbers: {
|
||||||
@ -450,6 +465,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"),
|
description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"),
|
||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||||
|
automationSettings: AddPageNumbersAutomationSettings,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["add-page-numbers"],
|
endpoints: ["add-page-numbers"],
|
||||||
operationConfig: addPageNumbersOperationConfig,
|
operationConfig: addPageNumbersOperationConfig,
|
||||||
@ -464,7 +480,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["multi-page-layout"],
|
endpoints: ["multi-page-layout"],
|
||||||
settingsComponent: PageLayoutSettings,
|
automationSettings: PageLayoutSettings,
|
||||||
synonyms: getSynonyms(t, "pageLayout")
|
synonyms: getSynonyms(t, "pageLayout")
|
||||||
},
|
},
|
||||||
bookletImposition: {
|
bookletImposition: {
|
||||||
@ -472,7 +488,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
name: t("home.bookletImposition.title", "Booklet Imposition"),
|
name: t("home.bookletImposition.title", "Booklet Imposition"),
|
||||||
component: BookletImposition,
|
component: BookletImposition,
|
||||||
operationConfig: bookletImpositionOperationConfig,
|
operationConfig: bookletImpositionOperationConfig,
|
||||||
settingsComponent: BookletImpositionSettings,
|
automationSettings: BookletImpositionSettings,
|
||||||
description: t("home.bookletImposition.desc", "Create booklets with proper page ordering and multi-page layout for printing and binding"),
|
description: t("home.bookletImposition.desc", "Create booklets with proper page ordering and multi-page layout for printing and binding"),
|
||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||||
@ -487,10 +503,10 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
urlPath: '/pdf-to-single-page',
|
|
||||||
endpoints: ["pdf-to-single-page"],
|
endpoints: ["pdf-to-single-page"],
|
||||||
operationConfig: singleLargePageOperationConfig,
|
operationConfig: singleLargePageOperationConfig,
|
||||||
synonyms: getSynonyms(t, "pdfToSinglePage")
|
synonyms: getSynonyms(t, "pdfToSinglePage"),
|
||||||
|
automationSettings: null,
|
||||||
},
|
},
|
||||||
addAttachments: {
|
addAttachments: {
|
||||||
icon: <LocalIcon icon="attachment-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="attachment-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -503,6 +519,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: 1,
|
maxFiles: 1,
|
||||||
endpoints: ["add-attachments"],
|
endpoints: ["add-attachments"],
|
||||||
operationConfig: addAttachmentsOperationConfig,
|
operationConfig: addAttachmentsOperationConfig,
|
||||||
|
automationSettings: AddAttachmentsSettings,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Extraction
|
// Extraction
|
||||||
@ -514,7 +531,8 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
description: t("home.extractPages.desc", "Extract specific pages from a PDF document"),
|
description: t("home.extractPages.desc", "Extract specific pages from a PDF document"),
|
||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.EXTRACTION,
|
subcategoryId: SubcategoryId.EXTRACTION,
|
||||||
synonyms: getSynonyms(t, "extractPages")
|
synonyms: getSynonyms(t, "extractPages"),
|
||||||
|
automationSettings: null,
|
||||||
},
|
},
|
||||||
extractImages: {
|
extractImages: {
|
||||||
icon: <LocalIcon icon="photo-library-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="photo-library-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -526,7 +544,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["extract-images"],
|
endpoints: ["extract-images"],
|
||||||
operationConfig: extractImagesOperationConfig,
|
operationConfig: extractImagesOperationConfig,
|
||||||
settingsComponent: ExtractImagesSettings,
|
automationSettings: ExtractImagesSettings,
|
||||||
synonyms: getSynonyms(t, "extractImages")
|
synonyms: getSynonyms(t, "extractImages")
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -541,7 +559,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
subcategoryId: SubcategoryId.REMOVAL,
|
subcategoryId: SubcategoryId.REMOVAL,
|
||||||
maxFiles: 1,
|
maxFiles: 1,
|
||||||
endpoints: ["remove-pages"],
|
endpoints: ["remove-pages"],
|
||||||
synonyms: getSynonyms(t, "removePages")
|
synonyms: getSynonyms(t, "removePages"),
|
||||||
|
operationConfig: removePagesOperationConfig,
|
||||||
|
automationSettings: RemovePagesSettings,
|
||||||
},
|
},
|
||||||
removeBlanks: {
|
removeBlanks: {
|
||||||
icon: <LocalIcon icon="scan-delete-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="scan-delete-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -552,7 +572,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
subcategoryId: SubcategoryId.REMOVAL,
|
subcategoryId: SubcategoryId.REMOVAL,
|
||||||
maxFiles: 1,
|
maxFiles: 1,
|
||||||
endpoints: ["remove-blanks"],
|
endpoints: ["remove-blanks"],
|
||||||
synonyms: getSynonyms(t, "removeBlanks")
|
synonyms: getSynonyms(t, "removeBlanks"),
|
||||||
|
operationConfig: removeBlanksOperationConfig,
|
||||||
|
automationSettings: RemoveBlanksSettings,
|
||||||
},
|
},
|
||||||
removeAnnotations: {
|
removeAnnotations: {
|
||||||
icon: <LocalIcon icon="thread-unread-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="thread-unread-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -563,7 +585,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
subcategoryId: SubcategoryId.REMOVAL,
|
subcategoryId: SubcategoryId.REMOVAL,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
operationConfig: removeAnnotationsOperationConfig,
|
operationConfig: removeAnnotationsOperationConfig,
|
||||||
settingsComponent: RemoveAnnotationsSettings,
|
automationSettings: null,
|
||||||
synonyms: getSynonyms(t, "removeAnnotations")
|
synonyms: getSynonyms(t, "removeAnnotations")
|
||||||
},
|
},
|
||||||
removeImage: {
|
removeImage: {
|
||||||
@ -577,6 +599,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
endpoints: ["remove-image-pdf"],
|
endpoints: ["remove-image-pdf"],
|
||||||
operationConfig: undefined,
|
operationConfig: undefined,
|
||||||
synonyms: getSynonyms(t, "removeImage"),
|
synonyms: getSynonyms(t, "removeImage"),
|
||||||
|
automationSettings: null,
|
||||||
},
|
},
|
||||||
removePassword: {
|
removePassword: {
|
||||||
icon: <LocalIcon icon="lock-open-right-outline-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="lock-open-right-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -588,7 +611,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
endpoints: ["remove-password"],
|
endpoints: ["remove-password"],
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
operationConfig: removePasswordOperationConfig,
|
operationConfig: removePasswordOperationConfig,
|
||||||
settingsComponent: RemovePasswordSettings,
|
automationSettings: RemovePasswordSettings,
|
||||||
synonyms: getSynonyms(t, "removePassword")
|
synonyms: getSynonyms(t, "removePassword")
|
||||||
},
|
},
|
||||||
removeCertSign: {
|
removeCertSign: {
|
||||||
@ -602,6 +625,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
endpoints: ["remove-certificate-sign"],
|
endpoints: ["remove-certificate-sign"],
|
||||||
operationConfig: removeCertificateSignOperationConfig,
|
operationConfig: removeCertificateSignOperationConfig,
|
||||||
synonyms: getSynonyms(t, "removeCertSign"),
|
synonyms: getSynonyms(t, "removeCertSign"),
|
||||||
|
automationSettings: null,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Automation
|
// Automation
|
||||||
@ -620,6 +644,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
supportedFormats: CONVERT_SUPPORTED_FORMATS,
|
supportedFormats: CONVERT_SUPPORTED_FORMATS,
|
||||||
endpoints: ["handleData"],
|
endpoints: ["handleData"],
|
||||||
synonyms: getSynonyms(t, "automate"),
|
synonyms: getSynonyms(t, "automate"),
|
||||||
|
automationSettings: null,
|
||||||
},
|
},
|
||||||
autoRename: {
|
autoRename: {
|
||||||
icon: <LocalIcon icon="match-word-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="match-word-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -632,6 +657,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.AUTOMATION,
|
subcategoryId: SubcategoryId.AUTOMATION,
|
||||||
synonyms: getSynonyms(t, "autoRename"),
|
synonyms: getSynonyms(t, "autoRename"),
|
||||||
|
automationSettings: null,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Advanced Formatting
|
// Advanced Formatting
|
||||||
@ -644,6 +670,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
||||||
synonyms: getSynonyms(t, "adjustContrast"),
|
synonyms: getSynonyms(t, "adjustContrast"),
|
||||||
|
automationSettings: null,
|
||||||
},
|
},
|
||||||
repair: {
|
repair: {
|
||||||
icon: <LocalIcon icon="build-outline-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="build-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -655,8 +682,8 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["repair"],
|
endpoints: ["repair"],
|
||||||
operationConfig: repairOperationConfig,
|
operationConfig: repairOperationConfig,
|
||||||
settingsComponent: RepairSettings,
|
synonyms: getSynonyms(t, "repair"),
|
||||||
synonyms: getSynonyms(t, "repair")
|
automationSettings: null
|
||||||
},
|
},
|
||||||
scannerImageSplit: {
|
scannerImageSplit: {
|
||||||
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -668,7 +695,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["extract-image-scans"],
|
endpoints: ["extract-image-scans"],
|
||||||
operationConfig: scannerImageSplitOperationConfig,
|
operationConfig: scannerImageSplitOperationConfig,
|
||||||
settingsComponent: ScannerImageSplitSettings,
|
automationSettings: ScannerImageSplitSettings,
|
||||||
synonyms: getSynonyms(t, "ScannerImageSplit"),
|
synonyms: getSynonyms(t, "ScannerImageSplit"),
|
||||||
},
|
},
|
||||||
overlayPdfs: {
|
overlayPdfs: {
|
||||||
@ -679,6 +706,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
||||||
synonyms: getSynonyms(t, "overlayPdfs"),
|
synonyms: getSynonyms(t, "overlayPdfs"),
|
||||||
|
automationSettings: null
|
||||||
},
|
},
|
||||||
replaceColor: {
|
replaceColor: {
|
||||||
icon: <LocalIcon icon="format-color-fill-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="format-color-fill-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -690,7 +718,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["replace-invert-pdf"],
|
endpoints: ["replace-invert-pdf"],
|
||||||
operationConfig: replaceColorOperationConfig,
|
operationConfig: replaceColorOperationConfig,
|
||||||
settingsComponent: ReplaceColorSettings,
|
automationSettings: ReplaceColorSettings,
|
||||||
synonyms: getSynonyms(t, "replaceColor"),
|
synonyms: getSynonyms(t, "replaceColor"),
|
||||||
},
|
},
|
||||||
addImage: {
|
addImage: {
|
||||||
@ -701,6 +729,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
||||||
synonyms: getSynonyms(t, "addImage"),
|
synonyms: getSynonyms(t, "addImage"),
|
||||||
|
automationSettings: null
|
||||||
},
|
},
|
||||||
editTableOfContents: {
|
editTableOfContents: {
|
||||||
icon: <LocalIcon icon="bookmark-add-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="bookmark-add-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -710,6 +739,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
||||||
synonyms: getSynonyms(t, "editTableOfContents"),
|
synonyms: getSynonyms(t, "editTableOfContents"),
|
||||||
|
automationSettings: null
|
||||||
},
|
},
|
||||||
scannerEffect: {
|
scannerEffect: {
|
||||||
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -719,6 +749,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
||||||
synonyms: getSynonyms(t, "scannerEffect"),
|
synonyms: getSynonyms(t, "scannerEffect"),
|
||||||
|
automationSettings: null
|
||||||
},
|
},
|
||||||
|
|
||||||
// Developer Tools
|
// Developer Tools
|
||||||
@ -731,6 +762,8 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
|
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
|
||||||
synonyms: getSynonyms(t, "showJS"),
|
synonyms: getSynonyms(t, "showJS"),
|
||||||
|
supportsAutomate: false,
|
||||||
|
automationSettings: null
|
||||||
},
|
},
|
||||||
devApi: {
|
devApi: {
|
||||||
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
|
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
|
||||||
@ -741,6 +774,8 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
|
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
|
||||||
link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html",
|
link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html",
|
||||||
synonyms: getSynonyms(t, "devApi"),
|
synonyms: getSynonyms(t, "devApi"),
|
||||||
|
supportsAutomate: false,
|
||||||
|
automationSettings: null
|
||||||
},
|
},
|
||||||
devFolderScanning: {
|
devFolderScanning: {
|
||||||
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
|
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
|
||||||
@ -751,6 +786,8 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
|
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
|
||||||
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/",
|
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/",
|
||||||
synonyms: getSynonyms(t, "devFolderScanning"),
|
synonyms: getSynonyms(t, "devFolderScanning"),
|
||||||
|
supportsAutomate: false,
|
||||||
|
automationSettings: null
|
||||||
},
|
},
|
||||||
devSsoGuide: {
|
devSsoGuide: {
|
||||||
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
|
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
|
||||||
@ -761,6 +798,8 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
|
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
|
||||||
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration",
|
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration",
|
||||||
synonyms: getSynonyms(t, "devSsoGuide"),
|
synonyms: getSynonyms(t, "devSsoGuide"),
|
||||||
|
supportsAutomate: false,
|
||||||
|
automationSettings: null
|
||||||
},
|
},
|
||||||
devAirgapped: {
|
devAirgapped: {
|
||||||
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
|
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
|
||||||
@ -771,6 +810,8 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
|
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
|
||||||
link: "https://docs.stirlingpdf.com/Pro/#activation",
|
link: "https://docs.stirlingpdf.com/Pro/#activation",
|
||||||
synonyms: getSynonyms(t, "devAirgapped"),
|
synonyms: getSynonyms(t, "devAirgapped"),
|
||||||
|
supportsAutomate: false,
|
||||||
|
automationSettings: null
|
||||||
},
|
},
|
||||||
|
|
||||||
// Recommended Tools
|
// Recommended Tools
|
||||||
@ -781,7 +822,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
description: t("home.compare.desc", "Compare two PDF documents and highlight differences"),
|
description: t("home.compare.desc", "Compare two PDF documents and highlight differences"),
|
||||||
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.GENERAL,
|
subcategoryId: SubcategoryId.GENERAL,
|
||||||
synonyms: getSynonyms(t, "compare")
|
synonyms: getSynonyms(t, "compare"),
|
||||||
|
supportsAutomate: false,
|
||||||
|
automationSettings: null
|
||||||
},
|
},
|
||||||
compress: {
|
compress: {
|
||||||
icon: <LocalIcon icon="zoom-in-map-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="zoom-in-map-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
@ -792,7 +835,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
subcategoryId: SubcategoryId.GENERAL,
|
subcategoryId: SubcategoryId.GENERAL,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
operationConfig: compressOperationConfig,
|
operationConfig: compressOperationConfig,
|
||||||
settingsComponent: CompressSettings,
|
automationSettings: CompressSettings,
|
||||||
synonyms: getSynonyms(t, "compress")
|
synonyms: getSynonyms(t, "compress")
|
||||||
},
|
},
|
||||||
convert: {
|
convert: {
|
||||||
@ -822,7 +865,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
],
|
],
|
||||||
|
|
||||||
operationConfig: convertOperationConfig,
|
operationConfig: convertOperationConfig,
|
||||||
settingsComponent: ConvertSettings,
|
automationSettings: ConvertSettings,
|
||||||
synonyms: getSynonyms(t, "convert")
|
synonyms: getSynonyms(t, "convert")
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -834,9 +877,8 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.GENERAL,
|
subcategoryId: SubcategoryId.GENERAL,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
urlPath: '/ocr-pdf',
|
|
||||||
operationConfig: ocrOperationConfig,
|
operationConfig: ocrOperationConfig,
|
||||||
settingsComponent: OCRSettings,
|
automationSettings: OCRSettings,
|
||||||
synonyms: getSynonyms(t, "ocr")
|
synonyms: getSynonyms(t, "ocr")
|
||||||
},
|
},
|
||||||
redact: {
|
redact: {
|
||||||
@ -849,7 +891,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["auto-redact"],
|
endpoints: ["auto-redact"],
|
||||||
operationConfig: redactOperationConfig,
|
operationConfig: redactOperationConfig,
|
||||||
settingsComponent: RedactSingleStepSettings,
|
automationSettings: RedactSingleStepSettings,
|
||||||
synonyms: getSynonyms(t, "redact")
|
synonyms: getSynonyms(t, "redact")
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation';
|
import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation';
|
||||||
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
|
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
|
||||||
import { ToolRegistry } from '../../../data/toolsTaxonomy';
|
import { ToolRegistry } from '../../../data/toolsTaxonomy';
|
||||||
|
import { ToolId } from 'src/types/toolId';
|
||||||
|
|
||||||
|
|
||||||
interface UseAutomationFormProps {
|
interface UseAutomationFormProps {
|
||||||
mode: AutomationMode;
|
mode: AutomationMode;
|
||||||
@ -41,11 +43,15 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
|
|||||||
const operations = existingAutomation.operations || [];
|
const operations = existingAutomation.operations || [];
|
||||||
const tools = operations.map((op, index) => {
|
const tools = operations.map((op, index) => {
|
||||||
const operation = typeof op === 'string' ? op : op.operation;
|
const operation = typeof op === 'string' ? op : op.operation;
|
||||||
|
const toolEntry = toolRegistry[operation as ToolId];
|
||||||
|
// If tool has no settingsComponent, it's automatically configured
|
||||||
|
const isConfigured = mode === AutomationMode.EDIT ? true : !toolEntry?.automationSettings;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${operation}-${Date.now()}-${index}`,
|
id: `${operation}-${Date.now()}-${index}`,
|
||||||
operation: operation,
|
operation: operation,
|
||||||
name: getToolName(operation),
|
name: getToolName(operation),
|
||||||
configured: mode === AutomationMode.EDIT ? true : false,
|
configured: isConfigured,
|
||||||
parameters: typeof op === 'object' ? op.parameters || {} : {}
|
parameters: typeof op === 'object' ? op.parameters || {} : {}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -65,11 +71,15 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
|
|||||||
}, [mode, existingAutomation, t, getToolName]);
|
}, [mode, existingAutomation, t, getToolName]);
|
||||||
|
|
||||||
const addTool = (operation: string) => {
|
const addTool = (operation: string) => {
|
||||||
|
const toolEntry = toolRegistry[operation as ToolId];
|
||||||
|
// If tool has no settingsComponent, it's automatically configured
|
||||||
|
const isConfigured = !toolEntry?.automationSettings;
|
||||||
|
|
||||||
const newTool: AutomationTool = {
|
const newTool: AutomationTool = {
|
||||||
id: `${operation}-${Date.now()}`,
|
id: `${operation}-${Date.now()}`,
|
||||||
operation,
|
operation,
|
||||||
name: getToolName(operation),
|
name: getToolName(operation),
|
||||||
configured: false,
|
configured: isConfigured,
|
||||||
parameters: getToolDefaultParameters(operation)
|
parameters: getToolDefaultParameters(operation)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import LocalIcon from '../../../components/shared/LocalIcon';
|
import LocalIcon from '../../../components/shared/LocalIcon';
|
||||||
import { SuggestedAutomation } from '../../../types/automation';
|
import { SuggestedAutomation } from '../../../types/automation';
|
||||||
|
import { SPLIT_METHODS } from '../../../constants/splitConstants';
|
||||||
|
|
||||||
// Create icon components
|
// Create icon components
|
||||||
const CompressIcon = () => React.createElement(LocalIcon, { icon: 'compress', width: '1.5rem', height: '1.5rem' });
|
const CompressIcon = () => React.createElement(LocalIcon, { icon: 'compress', width: '1.5rem', height: '1.5rem' });
|
||||||
@ -83,18 +84,18 @@ export function useSuggestedAutomations(): SuggestedAutomation[] {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
operation: "splitPdf",
|
operation: "split",
|
||||||
parameters: {
|
parameters: {
|
||||||
mode: 'bySizeOrCount',
|
method: SPLIT_METHODS.BY_SIZE,
|
||||||
pages: '',
|
pages: '',
|
||||||
hDiv: '1',
|
hDiv: '1',
|
||||||
vDiv: '1',
|
vDiv: '1',
|
||||||
merge: false,
|
merge: false,
|
||||||
splitType: 'size',
|
|
||||||
splitValue: '20MB',
|
splitValue: '20MB',
|
||||||
bookmarkLevel: '1',
|
bookmarkLevel: '1',
|
||||||
includeMetadata: false,
|
includeMetadata: false,
|
||||||
allowDuplicates: false,
|
allowDuplicates: false,
|
||||||
|
duplexMode: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export function useToolNavigation(): {
|
|||||||
|
|
||||||
const getToolNavigation = useCallback((toolId: string, tool: ToolRegistryEntry): ToolNavigationProps => {
|
const getToolNavigation = useCallback((toolId: string, tool: ToolRegistryEntry): ToolNavigationProps => {
|
||||||
// Generate SSR-safe relative path
|
// Generate SSR-safe relative path
|
||||||
const path = getToolUrlPath(toolId, tool);
|
const path = getToolUrlPath(toolId);
|
||||||
const href = path; // Relative path, no window.location needed
|
const href = path; // Relative path, no window.location needed
|
||||||
|
|
||||||
// Click handler that maintains SPA behavior
|
// Click handler that maintains SPA behavior
|
||||||
|
|||||||
@ -6,10 +6,8 @@ import { BaseToolProps, ToolComponent } from "../types/tool";
|
|||||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
import { useAddAttachmentsParameters } from "../hooks/tools/addAttachments/useAddAttachmentsParameters";
|
import { useAddAttachmentsParameters } from "../hooks/tools/addAttachments/useAddAttachmentsParameters";
|
||||||
import { useAddAttachmentsOperation } from "../hooks/tools/addAttachments/useAddAttachmentsOperation";
|
import { useAddAttachmentsOperation } from "../hooks/tools/addAttachments/useAddAttachmentsOperation";
|
||||||
import { Stack, Text, Group, ActionIcon, Alert, ScrollArea, Button } from "@mantine/core";
|
|
||||||
import LocalIcon from "../components/shared/LocalIcon";
|
|
||||||
import { useAccordionSteps } from "../hooks/tools/shared/useAccordionSteps";
|
import { useAccordionSteps } from "../hooks/tools/shared/useAccordionSteps";
|
||||||
// Removed FitText for two-line wrapping with clamping
|
import AddAttachmentsSettings from "../components/tools/addAttachments/AddAttachmentsSettings";
|
||||||
|
|
||||||
const AddAttachments = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const AddAttachments = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -67,99 +65,11 @@ const AddAttachments = ({ onPreviewFile, onComplete, onError }: BaseToolProps) =
|
|||||||
onCollapsedClick: () => accordion.handleStepToggle(AddAttachmentsStep.ATTACHMENTS),
|
onCollapsedClick: () => accordion.handleStepToggle(AddAttachmentsStep.ATTACHMENTS),
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
content: (
|
content: (
|
||||||
<Stack gap="md">
|
<AddAttachmentsSettings
|
||||||
<Alert color="blue" variant="light">
|
parameters={params.parameters}
|
||||||
<Text size="sm">
|
onParameterChange={params.updateParameter}
|
||||||
{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 = [...params.parameters.attachments, ...files];
|
|
||||||
params.updateParameter('attachments', newAttachments);
|
|
||||||
// Reset the input so the same file can be selected again
|
|
||||||
e.target.value = '';
|
|
||||||
}}
|
|
||||||
disabled={endpointLoading}
|
disabled={endpointLoading}
|
||||||
style={{ display: 'none' }}
|
|
||||||
id="attachments-input"
|
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
color="blue"
|
|
||||||
component="label"
|
|
||||||
htmlFor="attachments-input"
|
|
||||||
disabled={endpointLoading}
|
|
||||||
leftSection={<LocalIcon icon="plus" width="14" height="14" />}
|
|
||||||
>
|
|
||||||
{params.parameters.attachments.length > 0
|
|
||||||
? t("AddAttachmentsRequest.addMoreFiles", "Add more files...")
|
|
||||||
: t("AddAttachmentsRequest.placeholder", "Choose files...")
|
|
||||||
}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{params.parameters.attachments && params.parameters.attachments.length > 0 && (
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
{t("AddAttachmentsRequest.selectedFiles", "Selected Files")} ({params.parameters.attachments.length})
|
|
||||||
</Text>
|
|
||||||
<ScrollArea.Autosize mah={300} type="scroll" offsetScrollbars styles={{ viewport: { overflowX: 'hidden' } }}>
|
|
||||||
<Stack gap="xs">
|
|
||||||
{params.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 = params.parameters.attachments.filter((_, i) => i !== index);
|
|
||||||
params.updateParameter('attachments', newAttachments);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LocalIcon icon="close-rounded" width="14" height="14" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</ScrollArea.Autosize>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -6,10 +6,9 @@ import { BaseToolProps, ToolComponent } from "../types/tool";
|
|||||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
import { useAddPageNumbersParameters } from "../components/tools/addPageNumbers/useAddPageNumbersParameters";
|
import { useAddPageNumbersParameters } from "../components/tools/addPageNumbers/useAddPageNumbersParameters";
|
||||||
import { useAddPageNumbersOperation } from "../components/tools/addPageNumbers/useAddPageNumbersOperation";
|
import { useAddPageNumbersOperation } from "../components/tools/addPageNumbers/useAddPageNumbersOperation";
|
||||||
import { Select, Stack, TextInput, NumberInput, Divider, Text } from "@mantine/core";
|
|
||||||
import { Tooltip } from "../components/shared/Tooltip";
|
|
||||||
import PageNumberPreview from "../components/tools/addPageNumbers/PageNumberPreview";
|
|
||||||
import { useAccordionSteps } from "../hooks/tools/shared/useAccordionSteps";
|
import { useAccordionSteps } from "../hooks/tools/shared/useAccordionSteps";
|
||||||
|
import AddPageNumbersPositionSettings from "../components/tools/addPageNumbers/AddPageNumbersPositionSettings";
|
||||||
|
import AddPageNumbersAppearanceSettings from "../components/tools/addPageNumbers/AddPageNumbersAppearanceSettings";
|
||||||
|
|
||||||
const AddPageNumbers = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const AddPageNumbers = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -68,44 +67,13 @@ const AddPageNumbers = ({ onPreviewFile, onComplete, onError }: BaseToolProps) =
|
|||||||
onCollapsedClick: () => accordion.handleStepToggle(AddPageNumbersStep.POSITION_AND_PAGES),
|
onCollapsedClick: () => accordion.handleStepToggle(AddPageNumbersStep.POSITION_AND_PAGES),
|
||||||
isVisible: hasFiles || hasResults,
|
isVisible: hasFiles || hasResults,
|
||||||
content: (
|
content: (
|
||||||
<Stack gap="lg">
|
<AddPageNumbersPositionSettings
|
||||||
{/* Position Selection */}
|
|
||||||
<Stack gap="md">
|
|
||||||
<PageNumberPreview
|
|
||||||
parameters={params.parameters}
|
parameters={params.parameters}
|
||||||
onParameterChange={params.updateParameter}
|
onParameterChange={params.updateParameter}
|
||||||
|
disabled={endpointLoading}
|
||||||
file={selectedFiles[0] || null}
|
file={selectedFiles[0] || null}
|
||||||
showQuickGrid={true}
|
showQuickGrid={true}
|
||||||
/>
|
/>
|
||||||
</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={params.parameters.pagesToNumber}
|
|
||||||
onChange={(e) => params.updateParameter('pagesToNumber', e.currentTarget.value)}
|
|
||||||
placeholder={t('addPageNumbers.numberPagesDesc', 'e.g., 1,3,5-8 or leave blank for all pages')}
|
|
||||||
disabled={endpointLoading}
|
|
||||||
/>
|
|
||||||
</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={params.parameters.startingNumber}
|
|
||||||
onChange={(v) => params.updateParameter('startingNumber', typeof v === 'number' ? v : 1)}
|
|
||||||
min={1}
|
|
||||||
disabled={endpointLoading}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -116,56 +84,11 @@ const AddPageNumbers = ({ onPreviewFile, onComplete, onError }: BaseToolProps) =
|
|||||||
onCollapsedClick: () => accordion.handleStepToggle(AddPageNumbersStep.CUSTOMIZE),
|
onCollapsedClick: () => accordion.handleStepToggle(AddPageNumbersStep.CUSTOMIZE),
|
||||||
isVisible: hasFiles || hasResults,
|
isVisible: hasFiles || hasResults,
|
||||||
content: (
|
content: (
|
||||||
<Stack gap="md">
|
<AddPageNumbersAppearanceSettings
|
||||||
<Tooltip content={t('marginTooltip', 'Distance between the page number and the edge of the page.')}>
|
parameters={params.parameters}
|
||||||
<Select
|
onParameterChange={params.updateParameter}
|
||||||
label={t('addPageNumbers.selectText.2', 'Margin')}
|
|
||||||
value={params.parameters.customMargin}
|
|
||||||
onChange={(v) => params.updateParameter('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={endpointLoading}
|
disabled={endpointLoading}
|
||||||
/>
|
/>
|
||||||
</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={params.parameters.fontSize}
|
|
||||||
onChange={(v) => params.updateParameter('fontSize', typeof v === 'number' ? v : 12)}
|
|
||||||
min={1}
|
|
||||||
disabled={endpointLoading}
|
|
||||||
/>
|
|
||||||
</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={params.parameters.fontType}
|
|
||||||
onChange={(v) => params.updateParameter('fontType', (v as any) || 'Times')}
|
|
||||||
data={[
|
|
||||||
{ value: 'Times', label: 'Times Roman' },
|
|
||||||
{ value: 'Helvetica', label: 'Helvetica' },
|
|
||||||
{ value: 'Courier', label: 'Courier New' },
|
|
||||||
]}
|
|
||||||
disabled={endpointLoading}
|
|
||||||
/>
|
|
||||||
</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={params.parameters.customText}
|
|
||||||
onChange={(e) => params.updateParameter('customText', e.currentTarget.value)}
|
|
||||||
placeholder={t('addPageNumbers.customNumberDesc', 'e.g., "Page {n}" or leave blank for just numbers')}
|
|
||||||
disabled={endpointLoading}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -6,15 +6,14 @@ import { BaseToolProps, ToolComponent } from "../types/tool";
|
|||||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
import { useAddStampParameters } from "../components/tools/addStamp/useAddStampParameters";
|
import { useAddStampParameters } from "../components/tools/addStamp/useAddStampParameters";
|
||||||
import { useAddStampOperation } from "../components/tools/addStamp/useAddStampOperation";
|
import { useAddStampOperation } from "../components/tools/addStamp/useAddStampOperation";
|
||||||
import { Group, Select, Stack, Textarea, TextInput, ColorInput, Button, Slider, Text, NumberInput, Divider } from "@mantine/core";
|
import { Stack, Text } from "@mantine/core";
|
||||||
import StampPreview from "../components/tools/addStamp/StampPreview";
|
import StampPreview from "../components/tools/addStamp/StampPreview";
|
||||||
import LocalIcon from "../components/shared/LocalIcon";
|
|
||||||
import styles from "../components/tools/addStamp/StampPreview.module.css";
|
import styles from "../components/tools/addStamp/StampPreview.module.css";
|
||||||
import { Tooltip } from "../components/shared/Tooltip";
|
|
||||||
import ButtonSelector from "../components/shared/ButtonSelector";
|
import ButtonSelector from "../components/shared/ButtonSelector";
|
||||||
import { useAccordionSteps } from "../hooks/tools/shared/useAccordionSteps";
|
import { useAccordionSteps } from "../hooks/tools/shared/useAccordionSteps";
|
||||||
import ObscuredOverlay from "../components/shared/ObscuredOverlay";
|
import ObscuredOverlay from "../components/shared/ObscuredOverlay";
|
||||||
import { getDefaultFontSizeForAlphabet } from "../components/tools/addStamp/StampPreviewUtils";
|
import StampSetupSettings from "../components/tools/addStamp/StampSetupSettings";
|
||||||
|
import StampPositionFormattingSettings from "../components/tools/addStamp/StampPositionFormattingSettings";
|
||||||
|
|
||||||
const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -77,101 +76,15 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
onCollapsedClick: () => accordion.handleStepToggle(AddStampStep.STAMP_SETUP),
|
onCollapsedClick: () => accordion.handleStepToggle(AddStampStep.STAMP_SETUP),
|
||||||
isVisible: hasFiles || hasResults,
|
isVisible: hasFiles || hasResults,
|
||||||
content: (
|
content: (
|
||||||
<Stack gap="md">
|
<StampSetupSettings
|
||||||
<TextInput
|
parameters={params.parameters}
|
||||||
label={t('pageSelectionPrompt', 'Page Selection (e.g. 1,3,2 or 4-8,2,10-12 or 2n-1)')}
|
onParameterChange={params.updateParameter}
|
||||||
value={params.parameters.pageNumbers}
|
|
||||||
onChange={(e) => params.updateParameter('pageNumbers', e.currentTarget.value)}
|
|
||||||
disabled={endpointLoading}
|
disabled={endpointLoading}
|
||||||
/>
|
/>
|
||||||
<Divider/>
|
|
||||||
<div>
|
|
||||||
<Text size="sm" fw={500} mb="xs">{t('AddStampRequest.stampType', 'Stamp Type')}</Text>
|
|
||||||
<ButtonSelector
|
|
||||||
value={params.parameters.stampType}
|
|
||||||
onChange={(v: 'text' | 'image') => params.updateParameter('stampType', v)}
|
|
||||||
options={[
|
|
||||||
{ value: 'text', label: t('watermark.type.1', 'Text') },
|
|
||||||
{ value: 'image', label: t('watermark.type.2', 'Image') },
|
|
||||||
]}
|
|
||||||
disabled={endpointLoading}
|
|
||||||
buttonClassName={styles.modeToggleButton}
|
|
||||||
textClassName={styles.modeToggleButtonText}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{params.parameters.stampType === 'text' && (
|
|
||||||
<>
|
|
||||||
<Textarea
|
|
||||||
label={t('AddStampRequest.stampText', 'Stamp Text')}
|
|
||||||
value={params.parameters.stampText}
|
|
||||||
onChange={(e) => params.updateParameter('stampText', e.currentTarget.value)}
|
|
||||||
autosize
|
|
||||||
minRows={2}
|
|
||||||
disabled={endpointLoading}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label={t('AddStampRequest.alphabet', 'Alphabet')}
|
|
||||||
value={params.parameters.alphabet}
|
|
||||||
onChange={(v) => {
|
|
||||||
const nextAlphabet = (v as any) || 'roman';
|
|
||||||
params.updateParameter('alphabet', nextAlphabet);
|
|
||||||
const nextDefault = getDefaultFontSizeForAlphabet(nextAlphabet);
|
|
||||||
params.updateParameter('fontSize', nextDefault);
|
|
||||||
}}
|
|
||||||
data={[
|
|
||||||
{ value: 'roman', label: 'Roman' },
|
|
||||||
{ value: 'arabic', label: 'العربية' },
|
|
||||||
{ value: 'japanese', label: '日本語' },
|
|
||||||
{ value: 'korean', label: '한국어' },
|
|
||||||
{ value: 'chinese', label: '简体中文' },
|
|
||||||
{ value: 'thai', label: 'ไทย' },
|
|
||||||
]}
|
|
||||||
disabled={endpointLoading}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{params.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) params.updateParameter('stampImage', file);
|
|
||||||
}}
|
|
||||||
disabled={endpointLoading}
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
id="stamp-image-input"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
component="label"
|
|
||||||
htmlFor="stamp-image-input"
|
|
||||||
disabled={endpointLoading}
|
|
||||||
>
|
|
||||||
{t('chooseFile', 'Choose File')}
|
|
||||||
</Button>
|
|
||||||
{params.parameters.stampImage && (
|
|
||||||
<Stack gap="xs">
|
|
||||||
<img
|
|
||||||
src={URL.createObjectURL(params.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">
|
|
||||||
{params.parameters.stampImage.name}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Step 3: Formatting & Position
|
// Step 2: Formatting & Position
|
||||||
steps.push({
|
steps.push({
|
||||||
title: t("AddStampRequest.positionAndFormatting", "Position & Formatting"),
|
title: t("AddStampRequest.positionAndFormatting", "Position & Formatting"),
|
||||||
isCollapsed: accordion.getCollapsedState(AddStampStep.POSITION_FORMATTING),
|
isCollapsed: accordion.getCollapsedState(AddStampStep.POSITION_FORMATTING),
|
||||||
@ -209,151 +122,13 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<StampPositionFormattingSettings
|
||||||
{/* Icon pill buttons row */}
|
parameters={params.parameters}
|
||||||
<div className="flex justify-between gap-[0.5rem]">
|
onParameterChange={params.updateParameter}
|
||||||
<Tooltip content={t('AddStampRequest.rotation', 'Rotation')} position="top">
|
|
||||||
<Button
|
|
||||||
variant={params.parameters._activePill === 'rotation' ? 'filled' : 'outline'}
|
|
||||||
className="flex-1"
|
|
||||||
onClick={() => params.updateParameter('_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={params.parameters._activePill === 'opacity' ? 'filled' : 'outline'}
|
|
||||||
className="flex-1"
|
|
||||||
onClick={() => params.updateParameter('_activePill', 'opacity')}
|
|
||||||
>
|
|
||||||
<LocalIcon icon="opacity" width="1.1rem" height="1.1rem" />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip content={params.parameters.stampType === 'image' ? t('AddStampRequest.imageSize', 'Image Size') : t('AddStampRequest.fontSize', 'Font Size')} position="top">
|
|
||||||
<Button
|
|
||||||
variant={params.parameters._activePill === 'fontSize' ? 'filled' : 'outline'}
|
|
||||||
className="flex-1"
|
|
||||||
onClick={() => params.updateParameter('_activePill', 'fontSize')}
|
|
||||||
>
|
|
||||||
<LocalIcon icon="zoom-in-map-rounded" width="1.1rem" height="1.1rem" />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Single slider bound to selected pill */}
|
|
||||||
{params.parameters._activePill === 'fontSize' && (
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Text className={styles.labelText}>
|
|
||||||
{params.parameters.stampType === 'image'
|
|
||||||
? t('AddStampRequest.imageSize', 'Image Size')
|
|
||||||
: t('AddStampRequest.fontSize', 'Font Size')
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
<Group className={styles.sliderGroup} align="center">
|
|
||||||
<NumberInput
|
|
||||||
value={params.parameters.fontSize}
|
|
||||||
onChange={(v) => params.updateParameter('fontSize', typeof v === 'number' ? v : 1)}
|
|
||||||
min={1}
|
|
||||||
max={400}
|
|
||||||
step={1}
|
|
||||||
size="sm"
|
|
||||||
className={styles.numberInput}
|
|
||||||
disabled={endpointLoading}
|
disabled={endpointLoading}
|
||||||
/>
|
/>
|
||||||
<Slider
|
|
||||||
value={params.parameters.fontSize}
|
|
||||||
onChange={(v) => params.updateParameter('fontSize', v as number)}
|
|
||||||
min={1}
|
|
||||||
max={400}
|
|
||||||
step={1}
|
|
||||||
className={styles.slider}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
{params.parameters._activePill === 'rotation' && (
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Text className={styles.labelText}>{t('AddStampRequest.rotation', 'Rotation')}</Text>
|
|
||||||
<Group className={styles.sliderGroup} align="center">
|
|
||||||
<NumberInput
|
|
||||||
value={params.parameters.rotation}
|
|
||||||
onChange={(v) => params.updateParameter('rotation', typeof v === 'number' ? v : 0)}
|
|
||||||
min={-180}
|
|
||||||
max={180}
|
|
||||||
step={1}
|
|
||||||
size="sm"
|
|
||||||
className={styles.numberInput}
|
|
||||||
hideControls
|
|
||||||
disabled={endpointLoading}
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
value={params.parameters.rotation}
|
|
||||||
onChange={(v) => params.updateParameter('rotation', v as number)}
|
|
||||||
min={-180}
|
|
||||||
max={180}
|
|
||||||
step={1}
|
|
||||||
className={styles.sliderWide}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
{params.parameters._activePill === 'opacity' && (
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Text className={styles.labelText}>{t('AddStampRequest.opacity', 'Opacity')}</Text>
|
|
||||||
<Group className={styles.sliderGroup} align="center">
|
|
||||||
<NumberInput
|
|
||||||
value={params.parameters.opacity}
|
|
||||||
onChange={(v) => params.updateParameter('opacity', typeof v === 'number' ? v : 0)}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
size="sm"
|
|
||||||
className={styles.numberInput}
|
|
||||||
disabled={endpointLoading}
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
value={params.parameters.opacity}
|
|
||||||
onChange={(v) => params.updateParameter('opacity', v as number)}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
className={styles.slider}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
{/* Unified preview wrapped with obscured overlay if no stamp selected */}
|
||||||
{params.parameters.stampType !== 'image' && (
|
|
||||||
<ColorInput
|
|
||||||
label={t('AddStampRequest.customColor', 'Custom Text Color')}
|
|
||||||
value={params.parameters.customColor}
|
|
||||||
onChange={(value) => params.updateParameter('customColor', value)}
|
|
||||||
format="hex"
|
|
||||||
disabled={endpointLoading}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
{/* Margin selection appears when using quick grid (and for text stamps) */}
|
|
||||||
{(params.parameters.stampType === 'text' || (params.parameters.stampType === 'image' && quickPositionModeSelected)) && (
|
|
||||||
<Select
|
|
||||||
label={t('AddStampRequest.margin', 'Margin')}
|
|
||||||
value={params.parameters.customMargin}
|
|
||||||
onChange={(v) => params.updateParameter('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={endpointLoading}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
{/* Unified preview wrapped with obscured overlay if no stamp selected in step 4 */}
|
|
||||||
<ObscuredOverlay
|
<ObscuredOverlay
|
||||||
obscured={
|
obscured={
|
||||||
accordion.currentStep === AddStampStep.POSITION_FORMATTING &&
|
accordion.currentStep === AddStampStep.POSITION_FORMATTING &&
|
||||||
|
|||||||
@ -5,6 +5,127 @@ import { AutomationFileProcessor } from './automationFileProcessor';
|
|||||||
import { ToolType } from '../hooks/tools/shared/useToolOperation';
|
import { ToolType } from '../hooks/tools/shared/useToolOperation';
|
||||||
import { processResponse } from './toolResponseProcessor';
|
import { processResponse } from './toolResponseProcessor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process multi-file tool response (handles ZIP or single PDF responses)
|
||||||
|
*/
|
||||||
|
const processMultiFileResponse = async (
|
||||||
|
responseData: Blob,
|
||||||
|
responseHeaders: any,
|
||||||
|
files: File[],
|
||||||
|
filePrefix: string,
|
||||||
|
preserveBackendFilename?: boolean
|
||||||
|
): Promise<File[]> => {
|
||||||
|
// Multi-file responses are typically ZIP files, but may be single files (e.g. split with merge=true)
|
||||||
|
if (responseData.type === 'application/pdf' ||
|
||||||
|
(responseHeaders && responseHeaders['content-type'] === 'application/pdf')) {
|
||||||
|
// Single PDF response - use processResponse to respect preserveBackendFilename
|
||||||
|
const processedFiles = await processResponse(
|
||||||
|
responseData,
|
||||||
|
files,
|
||||||
|
filePrefix,
|
||||||
|
undefined,
|
||||||
|
preserveBackendFilename ? responseHeaders : undefined
|
||||||
|
);
|
||||||
|
return processedFiles;
|
||||||
|
} else {
|
||||||
|
// ZIP response
|
||||||
|
const result = await AutomationFileProcessor.extractAutomationZipFiles(responseData);
|
||||||
|
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
console.warn(`⚠️ File processing warnings:`, result.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply prefix to files, replacing any existing prefix
|
||||||
|
const processedFiles = filePrefix && !preserveBackendFilename
|
||||||
|
? result.files.map(file => {
|
||||||
|
const nameWithoutPrefix = file.name.replace(/^[^_]*_/, '');
|
||||||
|
return new File([file], `${filePrefix}${nameWithoutPrefix}`, { type: file.type });
|
||||||
|
})
|
||||||
|
: result.files;
|
||||||
|
|
||||||
|
return processedFiles;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core execution function for API requests
|
||||||
|
*/
|
||||||
|
const executeApiRequest = async (
|
||||||
|
endpoint: string,
|
||||||
|
formData: FormData,
|
||||||
|
files: File[],
|
||||||
|
filePrefix: string,
|
||||||
|
preserveBackendFilename?: boolean
|
||||||
|
): Promise<File[]> => {
|
||||||
|
const response = await axios.post(endpoint, formData, {
|
||||||
|
responseType: 'blob',
|
||||||
|
timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
||||||
|
});
|
||||||
|
|
||||||
|
return await processMultiFileResponse(
|
||||||
|
response.data,
|
||||||
|
response.headers,
|
||||||
|
files,
|
||||||
|
filePrefix,
|
||||||
|
preserveBackendFilename
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute single-file tool operation (processes files one at a time)
|
||||||
|
*/
|
||||||
|
const executeSingleFileOperation = async (
|
||||||
|
config: any,
|
||||||
|
parameters: any,
|
||||||
|
files: File[],
|
||||||
|
filePrefix: string
|
||||||
|
): Promise<File[]> => {
|
||||||
|
const resultFiles: File[] = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const endpoint = typeof config.endpoint === 'function'
|
||||||
|
? config.endpoint(parameters)
|
||||||
|
: config.endpoint;
|
||||||
|
|
||||||
|
const formData = (config.buildFormData as (params: any, file: File) => FormData)(parameters, file);
|
||||||
|
|
||||||
|
const processedFiles = await executeApiRequest(
|
||||||
|
endpoint,
|
||||||
|
formData,
|
||||||
|
[file],
|
||||||
|
filePrefix,
|
||||||
|
config.preserveBackendFilename
|
||||||
|
);
|
||||||
|
resultFiles.push(...processedFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultFiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute multi-file tool operation (processes all files in one request)
|
||||||
|
*/
|
||||||
|
const executeMultiFileOperation = async (
|
||||||
|
config: any,
|
||||||
|
parameters: any,
|
||||||
|
files: File[],
|
||||||
|
filePrefix: string
|
||||||
|
): Promise<File[]> => {
|
||||||
|
const endpoint = typeof config.endpoint === 'function'
|
||||||
|
? config.endpoint(parameters)
|
||||||
|
: config.endpoint;
|
||||||
|
|
||||||
|
const formData = (config.buildFormData as (params: any, files: File[]) => FormData)(parameters, files);
|
||||||
|
|
||||||
|
return await executeApiRequest(
|
||||||
|
endpoint,
|
||||||
|
formData,
|
||||||
|
files,
|
||||||
|
filePrefix,
|
||||||
|
config.preserveBackendFilename
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a tool operation directly without using React hooks
|
* Execute a tool operation directly without using React hooks
|
||||||
@ -28,119 +149,27 @@ export const executeToolOperationWithPrefix = async (
|
|||||||
toolRegistry: ToolRegistry,
|
toolRegistry: ToolRegistry,
|
||||||
filePrefix: string = AUTOMATION_CONSTANTS.FILE_PREFIX
|
filePrefix: string = AUTOMATION_CONSTANTS.FILE_PREFIX
|
||||||
): Promise<File[]> => {
|
): Promise<File[]> => {
|
||||||
console.log(`🔧 Executing tool: ${operationName}`, { parameters, fileCount: files.length });
|
|
||||||
|
|
||||||
const config = toolRegistry[operationName as keyof ToolRegistry]?.operationConfig;
|
const config = toolRegistry[operationName as keyof ToolRegistry]?.operationConfig;
|
||||||
if (!config) {
|
if (!config) {
|
||||||
console.error(`❌ Tool operation not supported: ${operationName}`);
|
|
||||||
throw new Error(`Tool operation not supported: ${operationName}`);
|
throw new Error(`Tool operation not supported: ${operationName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📋 Using config:`, config);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if tool uses custom processor (like Convert tool)
|
// Check if tool uses custom processor (like Convert tool)
|
||||||
if (config.customProcessor) {
|
if (config.customProcessor) {
|
||||||
console.log(`🎯 Using custom processor for ${config.operationType}`);
|
|
||||||
const resultFiles = await config.customProcessor(parameters, files);
|
const resultFiles = await config.customProcessor(parameters, files);
|
||||||
console.log(`✅ Custom processor returned ${resultFiles.length} files`);
|
|
||||||
return resultFiles;
|
return resultFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute based on tool type
|
||||||
if (config.toolType === ToolType.multiFile) {
|
if (config.toolType === ToolType.multiFile) {
|
||||||
// Multi-file processing - single API call with all files
|
return await executeMultiFileOperation(config, parameters, files, filePrefix);
|
||||||
const endpoint = typeof config.endpoint === 'function'
|
|
||||||
? config.endpoint(parameters)
|
|
||||||
: config.endpoint;
|
|
||||||
|
|
||||||
console.log(`🌐 Making multi-file request to: ${endpoint}`);
|
|
||||||
const formData = (config.buildFormData as (params: any, files: File[]) => FormData)(parameters, files);
|
|
||||||
console.log(`📤 FormData entries:`, Array.from(formData.entries()));
|
|
||||||
|
|
||||||
const response = await axios.post(endpoint, formData, {
|
|
||||||
responseType: 'blob',
|
|
||||||
timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`📥 Response status: ${response.status}, size: ${response.data.size} bytes`);
|
|
||||||
|
|
||||||
// Multi-file responses are typically ZIP files, but may be single files (e.g. split with merge=true)
|
|
||||||
let result;
|
|
||||||
if (response.data.type === 'application/pdf' ||
|
|
||||||
(response.headers && response.headers['content-type'] === 'application/pdf')) {
|
|
||||||
// Single PDF response (e.g. split with merge option) - use processResponse to respect preserveBackendFilename
|
|
||||||
const processedFiles = await processResponse(
|
|
||||||
response.data,
|
|
||||||
files,
|
|
||||||
filePrefix,
|
|
||||||
undefined,
|
|
||||||
config.preserveBackendFilename ? response.headers : undefined
|
|
||||||
);
|
|
||||||
result = {
|
|
||||||
success: true,
|
|
||||||
files: processedFiles,
|
|
||||||
errors: []
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
// ZIP response
|
return await executeSingleFileOperation(config, parameters, files, filePrefix);
|
||||||
result = await AutomationFileProcessor.extractAutomationZipFiles(response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.errors.length > 0) {
|
|
||||||
console.warn(`⚠️ File processing warnings:`, result.errors);
|
|
||||||
}
|
|
||||||
// Apply prefix to files, replacing any existing prefix
|
|
||||||
// Skip prefixing if preserveBackendFilename is true and backend provided a filename
|
|
||||||
const processedFiles = filePrefix && !config.preserveBackendFilename
|
|
||||||
? result.files.map(file => {
|
|
||||||
const nameWithoutPrefix = file.name.replace(/^[^_]*_/, '');
|
|
||||||
return new File([file], `${filePrefix}${nameWithoutPrefix}`, { type: file.type });
|
|
||||||
})
|
|
||||||
: result.files;
|
|
||||||
|
|
||||||
console.log(`📁 Processed ${processedFiles.length} files from response`);
|
|
||||||
return processedFiles;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Single-file processing - separate API call per file
|
|
||||||
console.log(`🔄 Processing ${files.length} files individually`);
|
|
||||||
const resultFiles: File[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
const file = files[i];
|
|
||||||
const endpoint = typeof config.endpoint === 'function'
|
|
||||||
? config.endpoint(parameters)
|
|
||||||
: config.endpoint;
|
|
||||||
|
|
||||||
console.log(`🌐 Making single-file request ${i+1}/${files.length} to: ${endpoint} for file: ${file.name}`);
|
|
||||||
const formData = (config.buildFormData as (params: any, file: File) => FormData)(parameters, file);
|
|
||||||
console.log(`📤 FormData entries:`, Array.from(formData.entries()));
|
|
||||||
|
|
||||||
const response = await axios.post(endpoint, formData, {
|
|
||||||
responseType: 'blob',
|
|
||||||
timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`📥 Response ${i+1} status: ${response.status}, size: ${response.data.size} bytes`);
|
|
||||||
|
|
||||||
// Create result file using processResponse to respect preserveBackendFilename setting
|
|
||||||
const processedFiles = await processResponse(
|
|
||||||
response.data,
|
|
||||||
[file],
|
|
||||||
filePrefix,
|
|
||||||
undefined,
|
|
||||||
config.preserveBackendFilename ? response.headers : undefined
|
|
||||||
);
|
|
||||||
resultFiles.push(...processedFiles);
|
|
||||||
console.log(`✅ Created result file(s): ${processedFiles.map(f => f.name).join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🎉 Single-file processing complete: ${resultFiles.length} files`);
|
|
||||||
return resultFiles;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`Tool operation ${operationName} failed:`, error);
|
console.error(`❌ ${operationName} failed:`, error);
|
||||||
throw new Error(`${operationName} operation failed: ${error.response?.data || error.message}`);
|
throw new Error(`${operationName} operation failed: ${error.response?.data || error.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -156,9 +185,8 @@ export const executeAutomationSequence = async (
|
|||||||
onStepComplete?: (stepIndex: number, resultFiles: File[]) => void,
|
onStepComplete?: (stepIndex: number, resultFiles: File[]) => void,
|
||||||
onStepError?: (stepIndex: number, error: string) => void
|
onStepError?: (stepIndex: number, error: string) => void
|
||||||
): Promise<File[]> => {
|
): Promise<File[]> => {
|
||||||
console.log(`🚀 Starting automation sequence: ${automation.name || 'Unnamed'}`);
|
console.log(`🚀 Starting automation: ${automation.name || 'Unnamed'}`);
|
||||||
console.log(`📁 Initial files: ${initialFiles.length}`);
|
console.log(`📁 Input: ${initialFiles.length} file(s)`);
|
||||||
console.log(`🔧 Operations: ${automation.operations?.length || 0}`);
|
|
||||||
|
|
||||||
if (!automation?.operations || automation.operations.length === 0) {
|
if (!automation?.operations || automation.operations.length === 0) {
|
||||||
throw new Error('No operations in automation');
|
throw new Error('No operations in automation');
|
||||||
@ -170,9 +198,8 @@ export const executeAutomationSequence = async (
|
|||||||
for (let i = 0; i < automation.operations.length; i++) {
|
for (let i = 0; i < automation.operations.length; i++) {
|
||||||
const operation = automation.operations[i];
|
const operation = automation.operations[i];
|
||||||
|
|
||||||
console.log(`📋 Step ${i + 1}/${automation.operations.length}: ${operation.operation}`);
|
console.log(`\n📋 Step ${i + 1}/${automation.operations.length}: ${operation.operation}`);
|
||||||
console.log(`📄 Input files: ${currentFiles.length}`);
|
console.log(` Input: ${currentFiles.length} file(s)`);
|
||||||
console.log(`⚙️ Parameters:`, operation.parameters || {});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
onStepStart?.(i, operation.operation);
|
onStepStart?.(i, operation.operation);
|
||||||
@ -196,6 +223,6 @@ export const executeAutomationSequence = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🎉 Automation sequence completed: ${currentFiles.length} final files`);
|
console.log(`\n🎉 Automation complete: ${currentFiles.length} file(s)`);
|
||||||
return currentFiles;
|
return currentFiles;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export function parseToolRoute(registry: ToolRegistry): ToolRoute {
|
|||||||
|
|
||||||
// Fallback: Try to find tool by primary URL path in registry
|
// Fallback: Try to find tool by primary URL path in registry
|
||||||
for (const [toolId, tool] of Object.entries(registry)) {
|
for (const [toolId, tool] of Object.entries(registry)) {
|
||||||
const toolUrlPath = getToolUrlPath(toolId, tool);
|
const toolUrlPath = getToolUrlPath(toolId);
|
||||||
if (path === toolUrlPath && isValidToolId(toolId)) {
|
if (path === toolUrlPath && isValidToolId(toolId)) {
|
||||||
return {
|
return {
|
||||||
workbench: getToolWorkbench(tool),
|
workbench: getToolWorkbench(tool),
|
||||||
@ -88,7 +88,7 @@ export function updateToolRoute(toolId: ToolId, registry: ToolRegistry, replace:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolPath = getToolUrlPath(toolId, tool);
|
const toolPath = getToolUrlPath(toolId);
|
||||||
const newPath = withBasePath(toolPath);
|
const newPath = withBasePath(toolPath);
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
@ -116,19 +116,3 @@ export function getToolDisplayName(toolId: ToolId, registry: ToolRegistry): stri
|
|||||||
return tool ? tool.name : toolId;
|
return tool ? tool.name : toolId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate shareable URL for current tool state using registry
|
|
||||||
*/
|
|
||||||
export function generateShareableUrl(toolId: ToolId | null, registry: ToolRegistry): string {
|
|
||||||
const baseUrl = window.location.origin;
|
|
||||||
|
|
||||||
if (!toolId || !registry[toolId]) {
|
|
||||||
return `${baseUrl}${BASE_PATH || ''}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tool = registry[toolId];
|
|
||||||
|
|
||||||
const toolPath = getToolUrlPath(toolId, tool);
|
|
||||||
const fullPath = withBasePath(toolPath);
|
|
||||||
return `${baseUrl}${fullPath}`;
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user