+
{renderToolSettings()}
diff --git a/frontend/src/components/tools/automate/ToolList.tsx b/frontend/src/components/tools/automate/ToolList.tsx
index a21fbdecf..3cb2b7a7e 100644
--- a/frontend/src/components/tools/automate/ToolList.tsx
+++ b/frontend/src/components/tools/automate/ToolList.tsx
@@ -34,11 +34,14 @@ export default function ToolList({
const handleToolSelect = (index: number, newOperation: string) => {
const defaultParams = getToolDefaultParameters(newOperation);
+ const toolEntry = toolRegistry[newOperation];
+ // If tool has no settingsComponent, it's automatically configured
+ const isConfigured = !toolEntry?.automationSettings;
onToolUpdate(index, {
operation: newOperation,
name: getToolName(newOperation),
- configured: false,
+ configured: isConfigured,
parameters: defaultParams,
});
};
diff --git a/frontend/src/components/tools/automate/ToolSelector.tsx b/frontend/src/components/tools/automate/ToolSelector.tsx
index 02ebef0c9..5cbc4b3ab 100644
--- a/frontend/src/components/tools/automate/ToolSelector.tsx
+++ b/frontend/src/components/tools/automate/ToolSelector.tsx
@@ -1,7 +1,7 @@
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Stack, Text, ScrollArea } from '@mantine/core';
-import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
+import { ToolRegistryEntry, getToolSupportsAutomate } from '../../../data/toolsTaxonomy';
import { useToolSections } from '../../../hooks/useToolSections';
import { renderToolButtons } from '../shared/renderToolButtons';
import ToolSearch from '../toolPicker/ToolSearch';
@@ -28,9 +28,11 @@ export default function ToolSelector({
const [shouldAutoFocus, setShouldAutoFocus] = useState(false);
const containerRef = useRef
(null);
- // Filter out excluded tools (like 'automate' itself)
+ // Filter out excluded tools (like 'automate' itself) and tools that don't support automation
const baseFilteredTools = useMemo(() => {
- return Object.entries(toolRegistry).filter(([key]) => !excludeTools.includes(key));
+ return Object.entries(toolRegistry).filter(([key, tool]) =>
+ !excludeTools.includes(key) && getToolSupportsAutomate(tool)
+ );
}, [toolRegistry, excludeTools]);
// Apply search filter
diff --git a/frontend/src/components/tools/certSign/CertSignAutomationSettings.tsx b/frontend/src/components/tools/certSign/CertSignAutomationSettings.tsx
new file mode 100644
index 000000000..74da5b745
--- /dev/null
+++ b/frontend/src/components/tools/certSign/CertSignAutomationSettings.tsx
@@ -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: (key: K, value: CertSignParameters[K]) => void;
+ disabled?: boolean;
+}
+
+const CertSignAutomationSettings = ({ parameters, onParameterChange, disabled = false }: CertSignAutomationSettingsProps) => {
+ return (
+
+ {/* Sign Mode Selection (Manual vs Auto) */}
+
+
+ {/* Certificate Format - only show for Manual mode */}
+ {parameters.signMode === 'MANUAL' && (
+
+ )}
+
+ {/* Certificate Files - only show for Manual mode */}
+ {parameters.signMode === 'MANUAL' && (
+
+ )}
+
+ {/* Signature Appearance Settings */}
+
+
+ );
+};
+
+export default CertSignAutomationSettings;
diff --git a/frontend/src/components/tools/crop/CropAutomationSettings.tsx b/frontend/src/components/tools/crop/CropAutomationSettings.tsx
new file mode 100644
index 000000000..a098d39af
--- /dev/null
+++ b/frontend/src/components/tools/crop/CropAutomationSettings.tsx
@@ -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: (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 (
+
+
+
+ );
+};
+
+export default CropAutomationSettings;
diff --git a/frontend/src/components/tools/crop/CropCoordinateInputs.tsx b/frontend/src/components/tools/crop/CropCoordinateInputs.tsx
new file mode 100644
index 000000000..72c1e3d79
--- /dev/null
+++ b/frontend/src/components/tools/crop/CropCoordinateInputs.tsx
@@ -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 (
+
+ {showAutomationInfo && (
+
+
+ {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.")}
+
+
+ )}
+
+
+ {t("crop.coordinates.title", "Position and Size")}
+
+
+
+ onCoordinateChange('x', value)}
+ disabled={disabled}
+ min={0}
+ max={pdfBounds?.actualWidth}
+ step={0.1}
+ decimalScale={1}
+ size={showAutomationInfo ? "sm" : "xs"}
+ />
+ onCoordinateChange('y', value)}
+ disabled={disabled}
+ min={0}
+ max={pdfBounds?.actualHeight}
+ step={0.1}
+ decimalScale={1}
+ size={showAutomationInfo ? "sm" : "xs"}
+ />
+
+
+
+ onCoordinateChange('width', value)}
+ disabled={disabled}
+ min={0.1}
+ max={pdfBounds?.actualWidth}
+ step={0.1}
+ decimalScale={1}
+ size={showAutomationInfo ? "sm" : "xs"}
+ />
+ onCoordinateChange('height', value)}
+ disabled={disabled}
+ min={0.1}
+ max={pdfBounds?.actualHeight}
+ step={0.1}
+ decimalScale={1}
+ size={showAutomationInfo ? "sm" : "xs"}
+ />
+
+
+ {showAutomationInfo && (
+
+
+ {t("crop.automation.reference", "Reference: A4 page is 595.28 × 841.89 points (210mm × 297mm). 1 inch = 72 points.")}
+
+
+ )}
+
+ );
+};
+
+export default CropCoordinateInputs;
diff --git a/frontend/src/components/tools/crop/CropSettings.tsx b/frontend/src/components/tools/crop/CropSettings.tsx
index dafcdeaab..624ab3b88 100644
--- a/frontend/src/components/tools/crop/CropSettings.tsx
+++ b/frontend/src/components/tools/crop/CropSettings.tsx
@@ -1,10 +1,11 @@
import { useMemo, useState, useEffect } from "react";
-import { Stack, Text, Box, Group, NumberInput, ActionIcon, Center, Alert } from "@mantine/core";
+import { Stack, Text, Box, Group, ActionIcon, Center, Alert } from "@mantine/core";
import { useTranslation } from "react-i18next";
import RestartAltIcon from "@mui/icons-material/RestartAlt";
import { CropParametersHook } from "../../../hooks/tools/crop/useCropParameters";
import { useSelectedFiles } from "../../../contexts/file/fileHooks";
import CropAreaSelector from "./CropAreaSelector";
+import CropCoordinateInputs from "./CropCoordinateInputs";
import { DEFAULT_CROP_AREA } from "../../../constants/cropConstants";
import { PAGE_SIZES } from "../../../constants/pageSizeConstants";
import {
@@ -190,71 +191,22 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
{/* Manual Coordinate Input */}
-
-
- {t("crop.coordinates.title", "Position and Size")}
-
+
-
- handleCoordinateChange('x', value)}
- disabled={disabled}
- min={0}
- max={pdfBounds.actualWidth}
- step={0.1}
- decimalScale={1}
- size="xs"
- />
- handleCoordinateChange('y', value)}
- disabled={disabled}
- min={0}
- max={pdfBounds.actualHeight}
- step={0.1}
- decimalScale={1}
- size="xs"
- />
-
-
-
- handleCoordinateChange('width', value)}
- disabled={disabled}
- min={0.1}
- max={pdfBounds.actualWidth}
- step={0.1}
- decimalScale={1}
- size="xs"
- />
- handleCoordinateChange('height', value)}
- disabled={disabled}
- min={0.1}
- max={pdfBounds.actualHeight}
- step={0.1}
- decimalScale={1}
- size="xs"
- />
-
-
-
- {/* Validation Alert */}
- {!isCropValid && (
-
-
- {t("crop.error.invalidArea", "Crop area extends beyond PDF boundaries")}
-
-
- )}
-
+ {/* Validation Alert */}
+ {!isCropValid && (
+
+
+ {t("crop.error.invalidArea", "Crop area extends beyond PDF boundaries")}
+
+
+ )}
);
};
diff --git a/frontend/src/components/tools/removePages/RemovePagesSettings.tsx b/frontend/src/components/tools/removePages/RemovePagesSettings.tsx
index 99856e29c..2d8d0b7d6 100644
--- a/frontend/src/components/tools/removePages/RemovePagesSettings.tsx
+++ b/frontend/src/components/tools/removePages/RemovePagesSettings.tsx
@@ -16,16 +16,17 @@ const RemovePagesSettings = ({ parameters, onParameterChange, disabled = false }
// Allow user to type naturally - don't normalize input in real-time
onParameterChange('pageNumbers', value);
};
+ console.log('Current pageNumbers input:', parameters.pageNumbers, disabled);
// Check if current input is valid
- const isValid = validatePageNumbers(parameters.pageNumbers);
- const hasValue = parameters.pageNumbers.trim().length > 0;
+ const isValid = validatePageNumbers(parameters.pageNumbers || '');
+ const hasValue = (parameters?.pageNumbers?.trim().length ?? 0) > 0;
return (
handlePageNumbersChange(event.currentTarget.value)}
placeholder={t('removePages.pageNumbers.placeholder', 'e.g., 1,3,5-8,10')}
disabled={disabled}
diff --git a/frontend/src/components/tools/rotate/RotateAutomationSettings.tsx b/frontend/src/components/tools/rotate/RotateAutomationSettings.tsx
new file mode 100644
index 000000000..8449c2000
--- /dev/null
+++ b/frontend/src/components/tools/rotate/RotateAutomationSettings.tsx
@@ -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: (key: K, value: RotateParameters[K]) => void;
+ disabled?: boolean;
+}
+
+const RotateAutomationSettings = ({ parameters, onParameterChange, disabled = false }: RotateAutomationSettingsProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t("rotate.selectRotation", "Select Rotation Angle (Clockwise)")}
+
+
+ onParameterChange('angle', value)}
+ options={[
+ { value: 0, label: "0°" },
+ { value: 90, label: "90°" },
+ { value: 180, label: "180°" },
+ { value: 270, label: "270°" },
+ ]}
+ disabled={disabled}
+ />
+
+ );
+};
+
+export default RotateAutomationSettings;
diff --git a/frontend/src/components/tools/split/SplitAutomationSettings.tsx b/frontend/src/components/tools/split/SplitAutomationSettings.tsx
new file mode 100644
index 000000000..a6ad634e5
--- /dev/null
+++ b/frontend/src/components/tools/split/SplitAutomationSettings.tsx
@@ -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: (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 (
+
+ {/* Method Selection */}
+
+ );
+};
+
+export default SplitAutomationSettings;
diff --git a/frontend/src/components/tools/toolPicker/ToolButton.tsx b/frontend/src/components/tools/toolPicker/ToolButton.tsx
index 236cbb49f..40cee33a7 100644
--- a/frontend/src/components/tools/toolPicker/ToolButton.tsx
+++ b/frontend/src/components/tools/toolPicker/ToolButton.tsx
@@ -1,10 +1,13 @@
import React from "react";
import { Button } from "@mantine/core";
+import { useTranslation } from "react-i18next";
import { Tooltip } from "../../shared/Tooltip";
import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
import { useToolNavigation } from "../../../hooks/useToolNavigation";
import { handleUnlessSpecialClick } from "../../../utils/clickHandlers";
import FitText from "../../shared/FitText";
+import { useHotkeys } from "../../../contexts/HotkeyContext";
+import HotkeyDisplay from "../../hotkeys/HotkeyDisplay";
interface ToolButtonProps {
id: string;
@@ -17,8 +20,11 @@ interface ToolButtonProps {
}
const ToolButton: React.FC = ({ id, tool, isSelected, onSelect, disableNavigation = false, matchedSynonym }) => {
+ const { t } = useTranslation();
// Special case: read and multiTool are navigational tools that are always available
const isUnavailable = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool';
+ const { hotkeys } = useHotkeys();
+ const binding = hotkeys[id];
const { getToolNavigation } = useToolNavigation();
const handleClick = (id: string) => {
@@ -37,7 +43,17 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect,
const tooltipContent = isUnavailable
? (Coming soon: {tool.description})
- : tool.description;
+ : (
+
+
{tool.description}
+ {binding && (
+
+ {t('settings.hotkeys.shortcut', 'Shortcut')}
+
+
+ )}
+
+ );
const buttonContent = (
<>
diff --git a/frontend/src/contexts/HotkeyContext.tsx b/frontend/src/contexts/HotkeyContext.tsx
new file mode 100644
index 000000000..fe9cbb600
--- /dev/null
+++ b/frontend/src/contexts/HotkeyContext.tsx
@@ -0,0 +1,211 @@
+import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
+import { HotkeyBinding, bindingEquals, bindingMatchesEvent, deserializeBindings, getDisplayParts, isMacLike, normalizeBinding, serializeBindings } from '../utils/hotkeys';
+import { useToolWorkflow } from './ToolWorkflowContext';
+import { ToolId } from '../types/toolId';
+
+interface HotkeyContextValue {
+ hotkeys: Record;
+ defaults: Record;
+ isMac: boolean;
+ updateHotkey: (toolId: string, binding: HotkeyBinding) => void;
+ resetHotkey: (toolId: string) => void;
+ isBindingAvailable: (binding: HotkeyBinding, excludeToolId?: string) => boolean;
+ pauseHotkeys: () => void;
+ resumeHotkeys: () => void;
+ areHotkeysPaused: boolean;
+ getDisplayParts: (binding: HotkeyBinding | null | undefined) => string[];
+}
+
+const HotkeyContext = createContext(undefined);
+
+const STORAGE_KEY = 'stirlingpdf.hotkeys';
+
+const KEY_ORDER: string[] = [
+ 'Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5', 'Digit6', 'Digit7', 'Digit8', 'Digit9', 'Digit0',
+ 'KeyQ', 'KeyW', 'KeyE', 'KeyR', 'KeyT', 'KeyY', 'KeyU', 'KeyI', 'KeyO', 'KeyP',
+ 'KeyA', 'KeyS', 'KeyD', 'KeyF', 'KeyG', 'KeyH', 'KeyJ', 'KeyK', 'KeyL',
+ 'KeyZ', 'KeyX', 'KeyC', 'KeyV', 'KeyB', 'KeyN', 'KeyM',
+ 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
+];
+
+const generateDefaultHotkeys = (toolIds: string[], macLike: boolean): Record => {
+ const defaults: Record = {};
+ let index = 0;
+ let useShift = false;
+
+ const nextBinding = (): HotkeyBinding => {
+ if (index >= KEY_ORDER.length) {
+ index = 0;
+ if (!useShift) {
+ useShift = true;
+ } else {
+ // If we somehow run out of combinations, wrap back around (unlikely given tool count)
+ useShift = false;
+ }
+ }
+
+ const code = KEY_ORDER[index];
+ index += 1;
+
+ return {
+ code,
+ alt: true,
+ shift: useShift,
+ meta: macLike,
+ ctrl: !macLike,
+ };
+ };
+
+ toolIds.forEach(toolId => {
+ defaults[toolId] = nextBinding();
+ });
+
+ return defaults;
+};
+
+const shouldIgnoreTarget = (target: EventTarget | null): boolean => {
+ if (!target || !(target instanceof HTMLElement)) {
+ return false;
+ }
+ const editable = target.closest('input, textarea, [contenteditable="true"], [role="textbox"]');
+ return Boolean(editable);
+};
+
+export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const { toolRegistry, handleToolSelect } = useToolWorkflow();
+ const isMac = useMemo(() => isMacLike(), []);
+ const [customBindings, setCustomBindings] = useState>(() => {
+ if (typeof window === 'undefined') {
+ return {};
+ }
+ return deserializeBindings(window.localStorage?.getItem(STORAGE_KEY));
+ });
+ const [areHotkeysPaused, setHotkeysPaused] = useState(false);
+
+ const toolIds = useMemo(() => Object.keys(toolRegistry), [toolRegistry]);
+
+ const defaults = useMemo(() => generateDefaultHotkeys(toolIds, isMac), [toolIds, isMac]);
+
+ // Remove bindings for tools that are no longer present
+ useEffect(() => {
+ setCustomBindings(prev => {
+ const next: Record = {};
+ let changed = false;
+ Object.entries(prev).forEach(([toolId, binding]) => {
+ if (toolRegistry[toolId]) {
+ next[toolId] = binding;
+ } else {
+ changed = true;
+ }
+ });
+ return changed ? next : prev;
+ });
+ }, [toolRegistry]);
+
+ const resolved = useMemo(() => {
+ const merged: Record = {};
+ toolIds.forEach(toolId => {
+ const custom = customBindings[toolId];
+ merged[toolId] = custom ? normalizeBinding(custom) : defaults[toolId];
+ });
+ return merged;
+ }, [customBindings, defaults, toolIds]);
+
+ useEffect(() => {
+ if (typeof window === 'undefined') {
+ return;
+ }
+ window.localStorage.setItem(STORAGE_KEY, serializeBindings(customBindings));
+ }, [customBindings]);
+
+ const isBindingAvailable = useCallback((binding: HotkeyBinding, excludeToolId?: string) => {
+ const normalized = normalizeBinding(binding);
+ return Object.entries(resolved).every(([toolId, existing]) => {
+ if (toolId === excludeToolId) {
+ return true;
+ }
+ return !bindingEquals(existing, normalized);
+ });
+ }, [resolved]);
+
+ const updateHotkey = useCallback((toolId: string, binding: HotkeyBinding) => {
+ setCustomBindings(prev => {
+ const normalized = normalizeBinding(binding);
+ const defaultsForTool = defaults[toolId];
+ const next = { ...prev };
+ if (defaultsForTool && bindingEquals(defaultsForTool, normalized)) {
+ delete next[toolId];
+ } else {
+ next[toolId] = normalized;
+ }
+ return next;
+ });
+ }, [defaults]);
+
+ const resetHotkey = useCallback((toolId: string) => {
+ setCustomBindings(prev => {
+ if (!(toolId in prev)) {
+ return prev;
+ }
+ const next = { ...prev };
+ delete next[toolId];
+ return next;
+ });
+ }, []);
+
+ const pauseHotkeys = useCallback(() => setHotkeysPaused(true), []);
+ const resumeHotkeys = useCallback(() => setHotkeysPaused(false), []);
+
+ useEffect(() => {
+ if (areHotkeysPaused) {
+ return;
+ }
+
+ const handler = (event: KeyboardEvent) => {
+ if (event.repeat) return;
+ if (shouldIgnoreTarget(event.target)) return;
+
+ const entries = Object.entries(resolved) as Array<[string, HotkeyBinding]>;
+ for (const [toolId, binding] of entries) {
+ if (bindingMatchesEvent(binding, event)) {
+ event.preventDefault();
+ event.stopPropagation();
+ handleToolSelect(toolId as ToolId);
+ break;
+ }
+ }
+ };
+
+ window.addEventListener('keydown', handler, true);
+ return () => {
+ window.removeEventListener('keydown', handler, true);
+ };
+ }, [resolved, areHotkeysPaused, handleToolSelect]);
+
+ const contextValue = useMemo(() => ({
+ hotkeys: resolved,
+ defaults,
+ isMac,
+ updateHotkey,
+ resetHotkey,
+ isBindingAvailable,
+ pauseHotkeys,
+ resumeHotkeys,
+ areHotkeysPaused,
+ getDisplayParts: (binding) => getDisplayParts(binding ?? null, isMac),
+ }), [resolved, defaults, isMac, updateHotkey, resetHotkey, isBindingAvailable, pauseHotkeys, resumeHotkeys, areHotkeysPaused]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useHotkeys = (): HotkeyContextValue => {
+ const context = useContext(HotkeyContext);
+ if (!context) {
+ throw new Error('useHotkeys must be used within a HotkeyProvider');
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx
index e38375daa..98a0fd37d 100644
--- a/frontend/src/contexts/ToolWorkflowContext.tsx
+++ b/frontend/src/contexts/ToolWorkflowContext.tsx
@@ -75,7 +75,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
// Tool management (from hook)
selectedToolKey: string | null;
selectedTool: ToolRegistryEntry | null;
- toolRegistry: any; // From useToolManagement
+ toolRegistry: Record;
getSelectedTool: (toolId: string | null) => ToolRegistryEntry | null;
// UI Actions
@@ -231,7 +231,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
// Filter tools based on search query with fuzzy matching (name, description, id, synonyms)
const filteredTools = useMemo(() => {
if (!toolRegistry) return [];
- return filterToolRegistryByQuery(toolRegistry as Record, state.searchQuery);
+ return filterToolRegistryByQuery(toolRegistry as ToolRegistry, state.searchQuery);
}, [toolRegistry, state.searchQuery]);
const isPanelVisible = useMemo(() =>
diff --git a/frontend/src/data/toolsTaxonomy.ts b/frontend/src/data/toolsTaxonomy.ts
index 7b62a0cb5..79890dc6d 100644
--- a/frontend/src/data/toolsTaxonomy.ts
+++ b/frontend/src/data/toolsTaxonomy.ts
@@ -37,14 +37,14 @@ export type ToolRegistryEntry = {
endpoints?: string[];
link?: string;
type?: string;
- // URL path for routing (e.g., '/split-pdfs', '/compress-pdf')
- urlPath?: string;
// Workbench type for navigation
workbench?: WorkbenchType;
// Operation configuration for automation
operationConfig?: ToolOperationConfig;
// Settings component for automation configuration
- settingsComponent?: React.ComponentType;
+ automationSettings: React.ComponentType | null;
+ // Whether this tool supports automation (defaults to true)
+ supportsAutomate?: boolean;
// Synonyms for search (optional)
synonyms?: string[];
}
@@ -130,8 +130,8 @@ export const getToolWorkbench = (tool: ToolRegistryEntry): WorkbenchType => {
/**
* Get URL path for a tool
*/
-export const getToolUrlPath = (toolId: string, tool: ToolRegistryEntry): string => {
- return tool.urlPath || `/${toolId.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
+export const getToolUrlPath = (toolId: string): string => {
+ 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 => {
return toolId in registry;
};
+
+/**
+ * Check if a tool supports automation (defaults to true)
+ */
+export const getToolSupportsAutomate = (tool: ToolRegistryEntry): boolean => {
+ return tool.supportsAutomate !== false;
+};
diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx
index 103de4356..eb5757a8e 100644
--- a/frontend/src/data/useTranslatedToolRegistry.tsx
+++ b/frontend/src/data/useTranslatedToolRegistry.tsx
@@ -61,22 +61,19 @@ import { cropOperationConfig } from "../hooks/tools/crop/useCropOperation";
import { removeAnnotationsOperationConfig } from "../hooks/tools/removeAnnotations/useRemoveAnnotationsOperation";
import { extractImagesOperationConfig } from "../hooks/tools/extractImages/useExtractImagesOperation";
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 SplitSettings from "../components/tools/split/SplitSettings";
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
import RemovePasswordSettings from "../components/tools/removePassword/RemovePasswordSettings";
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 OCRSettings from "../components/tools/ocr/OCRSettings";
import ConvertSettings from "../components/tools/convert/ConvertSettings";
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
-import CertificateTypeSettings from "../components/tools/certSign/CertificateTypeSettings";
import BookletImpositionSettings from "../components/tools/bookletImposition/BookletImpositionSettings";
import FlattenSettings from "../components/tools/flatten/FlattenSettings";
import RedactSingleStepSettings from "../components/tools/redact/RedactSingleStepSettings";
-import RotateSettings from "../components/tools/rotate/RotateSettings";
import Redact from "../tools/Redact";
import AdjustPageScale from "../tools/AdjustPageScale";
import ReplaceColor from "../tools/ReplaceColor";
@@ -89,15 +86,22 @@ import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustP
import ScannerImageSplitSettings from "../components/tools/scannerImageSplit/ScannerImageSplitSettings";
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
import SignSettings from "../components/tools/sign/SignSettings";
-import CropSettings from "../components/tools/crop/CropSettings";
import AddPageNumbers from "../tools/AddPageNumbers";
import { addPageNumbersOperationConfig } from "../components/tools/addPageNumbers/useAddPageNumbersOperation";
import RemoveAnnotations from "../tools/RemoveAnnotations";
-import RemoveAnnotationsSettings from "../components/tools/removeAnnotations/RemoveAnnotationsSettings";
import PageLayoutSettings from "../components/tools/pageLayout/PageLayoutSettings";
import ExtractImages from "../tools/ExtractImages";
import ExtractImagesSettings from "../components/tools/extractImages/ExtractImagesSettings";
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
@@ -199,6 +203,8 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1,
synonyms: getSynonyms(t, "multiTool"),
+ supportsAutomate: false,
+ automationSettings: null
},
merge: {
icon: ,
@@ -210,7 +216,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["merge-pdfs"],
operationConfig: mergeOperationConfig,
- settingsComponent: MergeSettings,
+ automationSettings: MergeSettings,
synonyms: getSynonyms(t, "merge")
},
// Signing
@@ -225,7 +231,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["cert-sign"],
operationConfig: certSignOperationConfig,
- settingsComponent: CertificateTypeSettings,
+ automationSettings: CertSignAutomationSettings,
},
sign: {
icon: ,
@@ -235,8 +241,9 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.SIGNING,
operationConfig: signOperationConfig,
- settingsComponent: SignSettings,
- synonyms: getSynonyms(t, "sign")
+ automationSettings: SignSettings, // TODO:: not all settings shown, suggested next tools shown
+ synonyms: getSynonyms(t, "sign"),
+ supportsAutomate: false, //TODO make support Sign
},
// Document Security
@@ -251,7 +258,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["add-password"],
operationConfig: addPasswordOperationConfig,
- settingsComponent: AddPasswordSettings,
+ automationSettings: AddPasswordSettings,
synonyms: getSynonyms(t, "addPassword")
},
watermark: {
@@ -264,7 +271,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
endpoints: ["add-watermark"],
operationConfig: addWatermarkOperationConfig,
- settingsComponent: AddWatermarkSingleStepSettings,
+ automationSettings: AddWatermarkSingleStepSettings,
synonyms: getSynonyms(t, "watermark")
},
addStamp: {
@@ -278,6 +285,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["add-stamp"],
operationConfig: addStampOperationConfig,
+ automationSettings: AddStampAutomationSettings,
},
sanitize: {
icon: ,
@@ -289,7 +297,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"),
endpoints: ["sanitize-pdf"],
operationConfig: sanitizeOperationConfig,
- settingsComponent: SanitizeSettings,
+ automationSettings: SanitizeSettings,
synonyms: getSynonyms(t, "sanitize")
},
flatten: {
@@ -302,7 +310,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["flatten"],
operationConfig: flattenOperationConfig,
- settingsComponent: FlattenSettings,
+ automationSettings: FlattenSettings,
synonyms: getSynonyms(t, "flatten")
},
unlockPDFForms: {
@@ -315,8 +323,8 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["unlock-pdf-forms"],
operationConfig: unlockPdfFormsOperationConfig,
- settingsComponent: UnlockPdfFormsSettings,
synonyms: getSynonyms(t, "unlockPDFForms"),
+ automationSettings: null
},
changePermissions: {
icon: ,
@@ -328,7 +336,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["add-password"],
operationConfig: changePermissionsOperationConfig,
- settingsComponent: ChangePermissionsSettings,
+ automationSettings: ChangePermissionsSettings,
synonyms: getSynonyms(t, "changePermissions"),
},
getPdfInfo: {
@@ -339,6 +347,8 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.VERIFICATION,
synonyms: getSynonyms(t, "getPdfInfo"),
+ supportsAutomate: false,
+ automationSettings: null
},
validateSignature: {
icon: ,
@@ -348,6 +358,7 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.VERIFICATION,
synonyms: getSynonyms(t, "validateSignature"),
+ automationSettings: null
},
// Document Review
@@ -363,7 +374,9 @@ export function useFlatToolRegistry(): ToolRegistry {
),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_REVIEW,
- synonyms: getSynonyms(t, "read")
+ synonyms: getSynonyms(t, "read"),
+ supportsAutomate: false,
+ automationSettings: null
},
changeMetadata: {
icon: ,
@@ -375,7 +388,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["update-metadata"],
operationConfig: changeMetadataOperationConfig,
- settingsComponent: ChangeMetadataSingleStep,
+ automationSettings: ChangeMetadataSingleStep,
synonyms: getSynonyms(t, "changeMetadata")
},
// Page Formatting
@@ -390,7 +403,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["crop"],
operationConfig: cropOperationConfig,
- settingsComponent: CropSettings,
+ automationSettings: CropAutomationSettings,
},
rotate: {
icon: ,
@@ -402,7 +415,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["rotate-pdf"],
operationConfig: rotateOperationConfig,
- settingsComponent: RotateSettings,
+ automationSettings: RotateAutomationSettings,
synonyms: getSynonyms(t, "rotate")
},
split: {
@@ -413,7 +426,7 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
operationConfig: splitOperationConfig,
- settingsComponent: SplitSettings,
+ automationSettings: SplitAutomationSettings,
synonyms: getSynonyms(t, "split")
},
reorganizePages: {
@@ -428,7 +441,9 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.PAGE_FORMATTING,
endpoints: ["rearrange-pages"],
operationConfig: reorganizePagesOperationConfig,
- synonyms: getSynonyms(t, "reorganizePages")
+ synonyms: getSynonyms(t, "reorganizePages"),
+ automationSettings: null
+
},
scalePages: {
icon: ,
@@ -440,7 +455,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["scale-pages"],
operationConfig: adjustPageScaleOperationConfig,
- settingsComponent: AdjustPageScaleSettings,
+ automationSettings: AdjustPageScaleSettings,
synonyms: getSynonyms(t, "scalePages")
},
addPageNumbers: {
@@ -450,6 +465,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
+ automationSettings: AddPageNumbersAutomationSettings,
maxFiles: -1,
endpoints: ["add-page-numbers"],
operationConfig: addPageNumbersOperationConfig,
@@ -464,7 +480,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.PAGE_FORMATTING,
maxFiles: -1,
endpoints: ["multi-page-layout"],
- settingsComponent: PageLayoutSettings,
+ automationSettings: PageLayoutSettings,
synonyms: getSynonyms(t, "pageLayout")
},
bookletImposition: {
@@ -472,7 +488,7 @@ export function useFlatToolRegistry(): ToolRegistry {
name: t("home.bookletImposition.title", "Booklet Imposition"),
component: BookletImposition,
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"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
@@ -487,10 +503,10 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
maxFiles: -1,
- urlPath: '/pdf-to-single-page',
endpoints: ["pdf-to-single-page"],
operationConfig: singleLargePageOperationConfig,
- synonyms: getSynonyms(t, "pdfToSinglePage")
+ synonyms: getSynonyms(t, "pdfToSinglePage"),
+ automationSettings: null,
},
addAttachments: {
icon: ,
@@ -503,6 +519,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: 1,
endpoints: ["add-attachments"],
operationConfig: addAttachmentsOperationConfig,
+ automationSettings: AddAttachmentsSettings,
},
// Extraction
@@ -514,7 +531,8 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.extractPages.desc", "Extract specific pages from a PDF document"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.EXTRACTION,
- synonyms: getSynonyms(t, "extractPages")
+ synonyms: getSynonyms(t, "extractPages"),
+ automationSettings: null,
},
extractImages: {
icon: ,
@@ -526,7 +544,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["extract-images"],
operationConfig: extractImagesOperationConfig,
- settingsComponent: ExtractImagesSettings,
+ automationSettings: ExtractImagesSettings,
synonyms: getSynonyms(t, "extractImages")
},
@@ -541,7 +559,9 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.REMOVAL,
maxFiles: 1,
endpoints: ["remove-pages"],
- synonyms: getSynonyms(t, "removePages")
+ synonyms: getSynonyms(t, "removePages"),
+ operationConfig: removePagesOperationConfig,
+ automationSettings: RemovePagesSettings,
},
removeBlanks: {
icon: ,
@@ -552,7 +572,9 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.REMOVAL,
maxFiles: 1,
endpoints: ["remove-blanks"],
- synonyms: getSynonyms(t, "removeBlanks")
+ synonyms: getSynonyms(t, "removeBlanks"),
+ operationConfig: removeBlanksOperationConfig,
+ automationSettings: RemoveBlanksSettings,
},
removeAnnotations: {
icon: ,
@@ -563,7 +585,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.REMOVAL,
maxFiles: -1,
operationConfig: removeAnnotationsOperationConfig,
- settingsComponent: RemoveAnnotationsSettings,
+ automationSettings: null,
synonyms: getSynonyms(t, "removeAnnotations")
},
removeImage: {
@@ -577,6 +599,7 @@ export function useFlatToolRegistry(): ToolRegistry {
endpoints: ["remove-image-pdf"],
operationConfig: undefined,
synonyms: getSynonyms(t, "removeImage"),
+ automationSettings: null,
},
removePassword: {
icon: ,
@@ -588,7 +611,7 @@ export function useFlatToolRegistry(): ToolRegistry {
endpoints: ["remove-password"],
maxFiles: -1,
operationConfig: removePasswordOperationConfig,
- settingsComponent: RemovePasswordSettings,
+ automationSettings: RemovePasswordSettings,
synonyms: getSynonyms(t, "removePassword")
},
removeCertSign: {
@@ -602,6 +625,7 @@ export function useFlatToolRegistry(): ToolRegistry {
endpoints: ["remove-certificate-sign"],
operationConfig: removeCertificateSignOperationConfig,
synonyms: getSynonyms(t, "removeCertSign"),
+ automationSettings: null,
},
// Automation
@@ -620,6 +644,7 @@ export function useFlatToolRegistry(): ToolRegistry {
supportedFormats: CONVERT_SUPPORTED_FORMATS,
endpoints: ["handleData"],
synonyms: getSynonyms(t, "automate"),
+ automationSettings: null,
},
autoRename: {
icon: ,
@@ -632,6 +657,7 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.AUTOMATION,
synonyms: getSynonyms(t, "autoRename"),
+ automationSettings: null,
},
// Advanced Formatting
@@ -644,6 +670,7 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
synonyms: getSynonyms(t, "adjustContrast"),
+ automationSettings: null,
},
repair: {
icon: ,
@@ -655,8 +682,8 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["repair"],
operationConfig: repairOperationConfig,
- settingsComponent: RepairSettings,
- synonyms: getSynonyms(t, "repair")
+ synonyms: getSynonyms(t, "repair"),
+ automationSettings: null
},
scannerImageSplit: {
icon: ,
@@ -668,7 +695,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["extract-image-scans"],
operationConfig: scannerImageSplitOperationConfig,
- settingsComponent: ScannerImageSplitSettings,
+ automationSettings: ScannerImageSplitSettings,
synonyms: getSynonyms(t, "ScannerImageSplit"),
},
overlayPdfs: {
@@ -679,6 +706,7 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
synonyms: getSynonyms(t, "overlayPdfs"),
+ automationSettings: null
},
replaceColor: {
icon: ,
@@ -690,7 +718,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["replace-invert-pdf"],
operationConfig: replaceColorOperationConfig,
- settingsComponent: ReplaceColorSettings,
+ automationSettings: ReplaceColorSettings,
synonyms: getSynonyms(t, "replaceColor"),
},
addImage: {
@@ -701,6 +729,7 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
synonyms: getSynonyms(t, "addImage"),
+ automationSettings: null
},
editTableOfContents: {
icon: ,
@@ -710,6 +739,7 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
synonyms: getSynonyms(t, "editTableOfContents"),
+ automationSettings: null
},
scannerEffect: {
icon: ,
@@ -719,6 +749,7 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
synonyms: getSynonyms(t, "scannerEffect"),
+ automationSettings: null
},
// Developer Tools
@@ -731,6 +762,8 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
synonyms: getSynonyms(t, "showJS"),
+ supportsAutomate: false,
+ automationSettings: null
},
devApi: {
icon: ,
@@ -741,6 +774,8 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html",
synonyms: getSynonyms(t, "devApi"),
+ supportsAutomate: false,
+ automationSettings: null
},
devFolderScanning: {
icon: ,
@@ -751,6 +786,8 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/",
synonyms: getSynonyms(t, "devFolderScanning"),
+ supportsAutomate: false,
+ automationSettings: null
},
devSsoGuide: {
icon: ,
@@ -761,6 +798,8 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration",
synonyms: getSynonyms(t, "devSsoGuide"),
+ supportsAutomate: false,
+ automationSettings: null
},
devAirgapped: {
icon: ,
@@ -771,6 +810,8 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Pro/#activation",
synonyms: getSynonyms(t, "devAirgapped"),
+ supportsAutomate: false,
+ automationSettings: null
},
// Recommended Tools
@@ -781,7 +822,9 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.compare.desc", "Compare two PDF documents and highlight differences"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
- synonyms: getSynonyms(t, "compare")
+ synonyms: getSynonyms(t, "compare"),
+ supportsAutomate: false,
+ automationSettings: null
},
compress: {
icon: ,
@@ -792,7 +835,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1,
operationConfig: compressOperationConfig,
- settingsComponent: CompressSettings,
+ automationSettings: CompressSettings,
synonyms: getSynonyms(t, "compress")
},
convert: {
@@ -822,7 +865,7 @@ export function useFlatToolRegistry(): ToolRegistry {
],
operationConfig: convertOperationConfig,
- settingsComponent: ConvertSettings,
+ automationSettings: ConvertSettings,
synonyms: getSynonyms(t, "convert")
},
@@ -834,9 +877,8 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1,
- urlPath: '/ocr-pdf',
operationConfig: ocrOperationConfig,
- settingsComponent: OCRSettings,
+ automationSettings: OCRSettings,
synonyms: getSynonyms(t, "ocr")
},
redact: {
@@ -849,7 +891,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["auto-redact"],
operationConfig: redactOperationConfig,
- settingsComponent: RedactSingleStepSettings,
+ automationSettings: RedactSingleStepSettings,
synonyms: getSynonyms(t, "redact")
},
};
diff --git a/frontend/src/hooks/tools/automate/useAutomationForm.ts b/frontend/src/hooks/tools/automate/useAutomationForm.ts
index 6ad8afe81..4b5dc7b5d 100644
--- a/frontend/src/hooks/tools/automate/useAutomationForm.ts
+++ b/frontend/src/hooks/tools/automate/useAutomationForm.ts
@@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next';
import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation';
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
import { ToolRegistry } from '../../../data/toolsTaxonomy';
+import { ToolId } from 'src/types/toolId';
+
interface UseAutomationFormProps {
mode: AutomationMode;
@@ -41,11 +43,15 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
const operations = existingAutomation.operations || [];
const tools = operations.map((op, index) => {
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 {
id: `${operation}-${Date.now()}-${index}`,
operation: operation,
name: getToolName(operation),
- configured: mode === AutomationMode.EDIT ? true : false,
+ configured: isConfigured,
parameters: typeof op === 'object' ? op.parameters || {} : {}
};
});
@@ -65,11 +71,15 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
}, [mode, existingAutomation, t, getToolName]);
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 = {
id: `${operation}-${Date.now()}`,
operation,
name: getToolName(operation),
- configured: false,
+ configured: isConfigured,
parameters: getToolDefaultParameters(operation)
};
diff --git a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts
index 970c14375..dfa2b79b5 100644
--- a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts
+++ b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import React from 'react';
import LocalIcon from '../../../components/shared/LocalIcon';
import { SuggestedAutomation } from '../../../types/automation';
+import { SPLIT_METHODS } from '../../../constants/splitConstants';
// Create icon components
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: {
- mode: 'bySizeOrCount',
+ method: SPLIT_METHODS.BY_SIZE,
pages: '',
hDiv: '1',
vDiv: '1',
merge: false,
- splitType: 'size',
splitValue: '20MB',
bookmarkLevel: '1',
includeMetadata: false,
allowDuplicates: false,
+ duplexMode: false,
}
},
{
diff --git a/frontend/src/hooks/useToolNavigation.ts b/frontend/src/hooks/useToolNavigation.ts
index 5c6fcf47c..1fdc8f2c7 100644
--- a/frontend/src/hooks/useToolNavigation.ts
+++ b/frontend/src/hooks/useToolNavigation.ts
@@ -22,7 +22,7 @@ export function useToolNavigation(): {
const getToolNavigation = useCallback((toolId: string, tool: ToolRegistryEntry): ToolNavigationProps => {
// Generate SSR-safe relative path
- const path = getToolUrlPath(toolId, tool);
+ const path = getToolUrlPath(toolId);
const href = path; // Relative path, no window.location needed
// Click handler that maintains SPA behavior
diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx
index f283a1caa..4b9514a3c 100644
--- a/frontend/src/pages/HomePage.tsx
+++ b/frontend/src/pages/HomePage.tsx
@@ -15,6 +15,7 @@ import RightRail from "../components/shared/RightRail";
import FileManager from "../components/FileManager";
import LocalIcon from "../components/shared/LocalIcon";
import { useFilesModalContext } from "../contexts/FilesModalContext";
+import AppConfigModal from "../components/shared/AppConfigModal";
import "./HomePage.css";
@@ -37,6 +38,7 @@ export default function HomePage() {
const sliderRef = useRef(null);
const [activeMobileView, setActiveMobileView] = useState("tools");
const isProgrammaticScroll = useRef(false);
+ const [configModalOpen, setConfigModalOpen] = useState(false);
const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo");
const brandIconSrc = `${BASE_PATH}/branding/StirlingPDFLogoNoText${
@@ -207,8 +209,20 @@ export default function HomePage() {
{t('quickAccess.files', 'Files')}
+