[V2] fix(automation): enhance parameter handling and default values across operations, fix error in ManyToOne tools (#5123)

# Description of Changes
This pull request focuses on improving the handling of tool parameters
in the frontend automation and tool operation system. The main changes
ensure that default parameter values are consistently applied, missing
or undefined values are handled gracefully, and custom tools like
Automate and Merge correctly track input file consumption. These updates
enhance reliability and reduce errors due to missing or improperly
formatted parameters.

**Parameter Handling and Defaults:**

* Tool configuration and automation forms now merge user-provided
parameters with tool-specific default parameters, guaranteeing all
required fields are present during initialization and execution
(`ToolConfigurationModal.tsx`, `useAutomationForm.ts`,
`automationExecutor.ts`).
* All tool operation hooks (e.g., Add Watermark, Merge, Sanitize, Split,
Compress, Change Permissions, OCR) have been updated to safely handle
missing or undefined parameter values by providing sensible defaults,
preventing runtime errors and backend issues
(`useAddWatermarkOperation.ts`, `useMergeOperation.ts`,
`useSanitizeOperation.ts`, `useSplitOperation.ts`,
`useCompressOperation.ts`, `useChangePermissionsOperation.ts`,
`useOCROperation.ts`).

**Tool Operation Configuration Enhancements:**

* The `BaseToolOperationConfig` interface now supports a
`consumesAllInputs` flag, allowing tools with complex input-output
relationships (like Automate and Merge) to mark all input files as
successfully processed when appropriate (`useToolOperation.ts`).
* The Automate tool operation is configured to use this new flag, and
the core hook logic is updated to respect it when determining success
(`useAutomateOperation.ts`, `useToolOperation.ts`).

**Registry and Configuration Consistency:**

* Default parameters are now explicitly included in tool operation
configs (e.g., Merge), ensuring consistency between registry definitions
and runtime behavior (`useMergeOperation.ts`).

**Dependency Updates:**

* React hooks and callbacks now correctly include dependencies related
to parameter defaults to avoid stale values in forms and modals
(`useAutomationForm.ts`).

These changes collectively improve the robustness and maintainability of
the automation and tool configuration system.


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [X] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
This commit is contained in:
Balázs Szücs
2026-01-20 15:57:44 +01:00
committed by GitHub
parent 8bb807471f
commit bd75ad042a
12 changed files with 56 additions and 38 deletions

View File

@@ -41,13 +41,16 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel,
// Initialize parameters from tool (which should contain defaults from registry)
useEffect(() => {
const toolConfig = toolRegistry[tool.operation as ToolId]?.operationConfig;
const defaultParams = toolConfig?.defaultParameters || {};
if (tool.parameters) {
setParameters(tool.parameters);
setParameters({ ...defaultParams, ...tool.parameters });
} else {
// Fallback to empty parameters if none provided
setParameters({});
// Fallback to default parameters if none provided
setParameters({ ...defaultParams });
}
}, [tool.parameters, tool.operation]);
}, [tool.parameters, tool.operation, toolRegistry]);
// Render the settings component
const renderToolSettings = () => {

View File

@@ -18,7 +18,7 @@ export const buildAddWatermarkFormData = (parameters: AddWatermarkParameters, fi
formData.append("watermarkImage", parameters.watermarkImage);
}
// Required parameters with correct formatting
// Required parameters with correct formatting (defaults merged in automationExecutor)
formData.append("fontSize", parameters.fontSize.toString());
formData.append("rotation", parameters.rotation.toString());
formData.append("opacity", (parameters.opacity / 100).toString()); // Convert percentage to decimal
@@ -26,9 +26,9 @@ export const buildAddWatermarkFormData = (parameters: AddWatermarkParameters, fi
formData.append("heightSpacer", parameters.heightSpacer.toString());
// Backend-expected parameters from user input
formData.append("alphabet", parameters.alphabet);
formData.append("customColor", parameters.customColor);
formData.append("convertPDFToImage", parameters.convertPDFToImage.toString());
formData.append("alphabet", parameters.alphabet || "");
formData.append("customColor", parameters.customColor || "");
formData.append("convertPDFToImage", (parameters.convertPDFToImage ?? false).toString());
return formData;
};

View File

@@ -38,7 +38,7 @@ export function useAutomateOperation() {
console.log(`✅ Automation completed, returning ${finalResults.length} files`);
return {
files: finalResults,
consumedAllInputs: false,
consumedAllInputs: true,
};
}, [toolRegistry]);
@@ -46,5 +46,6 @@ export function useAutomateOperation() {
toolType: ToolType.custom,
operationType: 'automate',
customProcessor,
consumesAllInputs: true,
});
}

View File

@@ -52,7 +52,7 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
operation: operation,
name: getToolName(operation),
configured: isConfigured,
parameters: typeof op === 'object' ? op.parameters || {} : {}
parameters: typeof op === 'object' ? { ...getToolDefaultParameters(operation), ...(op.parameters || {}) } : getToolDefaultParameters(operation)
};
});
@@ -68,7 +68,7 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
}));
setSelectedTools(defaultTools);
}
}, [mode, existingAutomation, t, getToolName]);
}, [mode, existingAutomation, t, getToolName, getToolDefaultParameters]);
const addTool = (operation: string) => {
const toolEntry = toolRegistry[operation as ToolId];

View File

@@ -3,11 +3,12 @@ import { ToolType, useToolOperation } from '@app/hooks/tools/shared/useToolOpera
import { createStandardErrorHandler } from '@app/utils/toolErrorHandler';
import { ChangePermissionsParameters, defaultParameters } from '@app/hooks/tools/changePermissions/useChangePermissionsParameters';
export const getFormData = ((parameters: ChangePermissionsParameters) =>
Object.entries(parameters).map(([key, value]) =>
[key, value.toString()]
) as string[][]
);
export const getFormData = ((parameters: ChangePermissionsParameters) => {
if (!parameters) return [];
return Object.entries(parameters).map(([key, value]) =>
[key, (value ?? false).toString()]
) as string[][];
});
// Static function that can be used by both the hook and automation executor
export const buildChangePermissionsFormData = (parameters: ChangePermissionsParameters, file: File): FormData => {

View File

@@ -18,7 +18,7 @@ export const buildCompressFormData = (parameters: CompressParameters, file: File
}
}
formData.append("grayscale", parameters.grayscale.toString());
formData.append("grayscale", (parameters.grayscale ?? false).toString());
formData.append("lineArt", parameters.lineArt.toString());
formData.append("linearize", parameters.linearize.toString());
if (parameters.lineArt) {

View File

@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next';
import { useToolOperation, ToolOperationConfig, ToolType } from '@app/hooks/tools/shared/useToolOperation';
import { createStandardErrorHandler } from '@app/utils/toolErrorHandler';
import { MergeParameters } from '@app/hooks/tools/merge/useMergeParameters';
import { MergeParameters, defaultParameters } from '@app/hooks/tools/merge/useMergeParameters';
const buildFormData = (parameters: MergeParameters, files: File[]): FormData => {
const formData = new FormData();
@@ -13,8 +13,8 @@ const buildFormData = (parameters: MergeParameters, files: File[]): FormData =>
const clientIds: string[] = files.map((f: any) => String((f as any).fileId || f.name));
formData.append('clientFileIds', JSON.stringify(clientIds));
formData.append("sortType", "orderProvided"); // Always use orderProvided since UI handles sorting
formData.append("removeCertSign", parameters.removeDigitalSignature.toString());
formData.append("generateToc", parameters.generateTableOfContents.toString());
formData.append("removeCertSign", (parameters.removeDigitalSignature ?? false).toString());
formData.append("generateToc", (parameters.generateTableOfContents ?? false).toString());
return formData;
};
@@ -26,6 +26,7 @@ export const mergeOperationConfig: ToolOperationConfig<MergeParameters> = {
operationType: 'merge',
endpoint: '/api/v1/general/merge-pdfs',
filePrefix: 'merged_',
defaultParameters,
};
export const useMergeOperation = () => {

View File

@@ -44,11 +44,13 @@ export const buildOCRFormData = (parameters: OCRParameters, file: File): FormDat
parameters.languages.forEach((lang) => formData.append('languages', lang));
formData.append('ocrType', parameters.ocrType);
formData.append('ocrRenderType', parameters.ocrRenderType);
formData.append('sidecar', parameters.additionalOptions.includes('sidecar').toString());
formData.append('deskew', parameters.additionalOptions.includes('deskew').toString());
formData.append('clean', parameters.additionalOptions.includes('clean').toString());
formData.append('cleanFinal', parameters.additionalOptions.includes('cleanFinal').toString());
formData.append('removeImagesAfter', parameters.additionalOptions.includes('removeImagesAfter').toString());
const options = parameters.additionalOptions || [];
formData.append('sidecar', options.includes('sidecar').toString());
formData.append('deskew', options.includes('deskew').toString());
formData.append('clean', options.includes('clean').toString());
formData.append('cleanFinal', options.includes('cleanFinal').toString());
formData.append('removeImagesAfter', options.includes('removeImagesAfter').toString());
return formData;
};

View File

@@ -9,12 +9,12 @@ export const buildSanitizeFormData = (parameters: SanitizeParameters, file: File
formData.append('fileInput', file);
// Add parameters
formData.append('removeJavaScript', parameters.removeJavaScript.toString());
formData.append('removeEmbeddedFiles', parameters.removeEmbeddedFiles.toString());
formData.append('removeXMPMetadata', parameters.removeXMPMetadata.toString());
formData.append('removeMetadata', parameters.removeMetadata.toString());
formData.append('removeLinks', parameters.removeLinks.toString());
formData.append('removeFonts', parameters.removeFonts.toString());
formData.append('removeJavaScript', (parameters.removeJavaScript ?? false).toString());
formData.append('removeEmbeddedFiles', (parameters.removeEmbeddedFiles ?? false).toString());
formData.append('removeXMPMetadata', (parameters.removeXMPMetadata ?? false).toString());
formData.append('removeMetadata', (parameters.removeMetadata ?? false).toString());
formData.append('removeLinks', (parameters.removeLinks ?? false).toString());
formData.append('removeFonts', (parameters.removeFonts ?? false).toString());
return formData;
};

View File

@@ -71,6 +71,13 @@ interface BaseToolOperationConfig<TParams> {
/** Default parameter values for automation */
defaultParameters?: TParams;
/**
* For custom tools: if true, success implies all input files were successfully processed.
* Use this for tools like Automate or Merge where Many-to-One relationships exist
* and exact input-output mapping is difficult.
*/
consumesAllInputs?: boolean;
}
export interface SingleFileToolOperationConfig<TParams> extends BaseToolOperationConfig<TParams> {

View File

@@ -19,7 +19,7 @@ export const buildSplitFormData = (parameters: SplitParameters, file: File): For
case SPLIT_METHODS.BY_SECTIONS:
formData.append("horizontalDivisions", parameters.hDiv);
formData.append("verticalDivisions", parameters.vDiv);
formData.append("merge", parameters.merge.toString());
formData.append("merge", (parameters.merge ?? false).toString());
formData.append("splitMode", parameters.splitMode || 'SPLIT_ALL');
if (parameters.splitMode === 'CUSTOM' && parameters.customPages) {
formData.append("pageNumbers", parameters.customPages);
@@ -39,11 +39,11 @@ export const buildSplitFormData = (parameters: SplitParameters, file: File): For
break;
case SPLIT_METHODS.BY_CHAPTERS:
formData.append("bookmarkLevel", parameters.bookmarkLevel);
formData.append("includeMetadata", parameters.includeMetadata.toString());
formData.append("allowDuplicates", parameters.allowDuplicates.toString());
formData.append("includeMetadata", (parameters.includeMetadata ?? false).toString());
formData.append("allowDuplicates", (parameters.allowDuplicates ?? false).toString());
break;
case SPLIT_METHODS.BY_PAGE_DIVIDER:
formData.append("duplexMode", parameters.duplexMode.toString());
formData.append("duplexMode", (parameters.duplexMode ?? false).toString());
break;
default:
throw new Error(`Unknown split method: ${parameters.method}`);

View File

@@ -155,18 +155,21 @@ export const executeToolOperationWithPrefix = async (
throw new Error(`Tool operation not supported: ${operationName}`);
}
// Merge with default parameters to ensure all required fields are present
const mergedParameters = { ...config.defaultParameters, ...parameters };
try {
// Check if tool uses custom processor (like Convert tool)
if (config.customProcessor) {
const result = await config.customProcessor(parameters, files);
const result = await config.customProcessor(mergedParameters, files);
return result.files;
}
// Execute based on tool type
if (config.toolType === ToolType.multiFile) {
return await executeMultiFileOperation(config, parameters, files, filePrefix);
return await executeMultiFileOperation(config, mergedParameters, files, filePrefix);
} else {
return await executeSingleFileOperation(config, parameters, files, filePrefix);
return await executeSingleFileOperation(config, mergedParameters, files, filePrefix);
}
} catch (error: any) {