From 041b6a4b5fb8649b61606e559defc93a45e5ccd7 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Fri, 31 Jul 2026 16:59:16 +0100 Subject: [PATCH] Move parameter validation to config-accessible function (#7224) # Description of Changes Move the logic for validating params into global exported functions and attach them to the operation config so that we can access them elsewhere so things like Pipelines know whether the tool has been configured and provide a warning if not. image --- .../public/locales/en-US/translation.toml | 3 +- .../useAddPageNumbersOperation.ts | 2 + .../useAddPageNumbersParameters.ts | 10 ++- .../tools/addStamp/useAddStampOperation.ts | 2 + .../tools/addStamp/useAddStampParameters.ts | 18 ++-- .../addPassword/useAddPasswordOperation.ts | 2 + .../addPassword/useAddPasswordParameters.ts | 11 +++ .../addWatermark/useAddWatermarkOperation.ts | 2 + .../addWatermark/useAddWatermarkParameters.ts | 26 +++--- .../useAdjustPageScaleOperation.ts | 2 + .../useAdjustPageScaleParameters.ts | 12 ++- .../autoRotate/useAutoRotateOperation.ts | 2 + .../autoRotate/useAutoRotateParameters.ts | 8 +- .../useBookletImpositionOperation.ts | 2 + .../useBookletImpositionParameters.ts | 12 ++- .../tools/certSign/useCertSignOperation.ts | 2 + .../tools/certSign/useCertSignParameters.ts | 66 +++++++------- .../useChangeMetadataOperation.ts | 2 + .../useChangeMetadataParameters.ts | 6 +- .../useChangePermissionsOperation.ts | 2 + .../useChangePermissionsParameters.ts | 11 +++ .../tools/compare/useCompareParameters.ts | 17 ++-- .../tools/compress/useCompressOperation.ts | 2 + .../tools/compress/useCompressParameters.ts | 28 +++--- .../tools/convert/useConvertOperation.ts | 2 + .../tools/convert/useConvertParameters.ts | 6 +- .../core/hooks/tools/crop/useCropOperation.ts | 2 + .../hooks/tools/crop/useCropParameters.ts | 14 +-- .../extractPages/useExtractPagesOperation.ts | 2 + .../extractPages/useExtractPagesParameters.ts | 10 ++- .../core/hooks/tools/ocr/useOCROperation.ts | 2 + .../core/hooks/tools/ocr/useOCRParameters.ts | 12 ++- .../overlayPdfs/useOverlayPdfsOperation.ts | 2 + .../overlayPdfs/useOverlayPdfsParameters.ts | 29 ++++--- .../pageLayout/usePageLayoutOperation.ts | 2 + .../pageLayout/usePageLayoutParameters.ts | 70 ++++++++------- .../hooks/tools/redact/useRedactOperation.ts | 2 + .../hooks/tools/redact/useRedactParameters.ts | 24 ++--- .../removeBlanks/useRemoveBlanksOperation.ts | 2 + .../removeBlanks/useRemoveBlanksParameters.ts | 19 ++-- .../removePages/useRemovePagesOperation.ts | 2 + .../removePages/useRemovePagesParameters.ts | 10 ++- .../useRemovePasswordOperation.ts | 2 + .../useRemovePasswordParameters.ts | 12 ++- .../replaceColor/useReplaceColorOperation.ts | 2 + .../replaceColor/useReplaceColorParameters.ts | 14 ++- .../hooks/tools/rotate/useRotateOperation.ts | 2 + .../hooks/tools/rotate/useRotateParameters.ts | 12 ++- .../tools/sanitize/useSanitizeOperation.ts | 2 + .../tools/sanitize/useSanitizeParameters.ts | 12 ++- .../useScannerImageSplitOperation.ts | 2 + .../useScannerImageSplitParameters.ts | 14 ++- .../core/hooks/tools/shared/toolAutomation.ts | 20 +++++ .../hooks/tools/shared/toolOperationTypes.ts | 8 ++ .../shared/validateParamsCoverage.test.ts | 87 +++++++++++++++++++ .../core/hooks/tools/sign/useSignOperation.ts | 2 + .../hooks/tools/sign/useSignParameters.ts | 2 +- .../hooks/tools/split/useSplitOperation.ts | 2 + .../hooks/tools/split/useSplitParameters.ts | 58 +++++++------ .../timestampPdf/useTimestampPdfOperation.ts | 2 + .../timestampPdf/useTimestampPdfParameters.ts | 12 ++- .../src/portal/views/PipelineBuilder.tsx | 16 +++- 62 files changed, 541 insertions(+), 204 deletions(-) create mode 100644 frontend/editor/src/core/hooks/tools/shared/validateParamsCoverage.test.ts diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 045ea581f4..3cfc28cd09 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -7749,6 +7749,7 @@ inputs = "Input" inputSource = "Input source" inputTrigger = "Trigger" keepEditing = "Keep editing" +needsConfiguring = "Needs setting up" needsUpload = "Needs an uploaded file" noSources = "No sources connected yet. Create one to use it as an input." noToolMatches = "No tools match your search." @@ -7757,7 +7758,7 @@ searchTools = "Search tools" selectToolBody = "Add a tool to build your pipeline." selectToolTitle = "No tools yet" sendToSystem = "Send to another system" -stepsNeedSetup = "These steps still need an operation and an account chosen before saving: {{tools}}." +stepsNeedSetup = "These steps still need setting up before saving: {{tools}}." toolSettings = "Tool settings" unknownStep = "Unrecognized operation, kept as-is." unsavedBody = "You have unsaved changes. Save them before leaving, or discard them?" diff --git a/frontend/editor/src/core/components/tools/addPageNumbers/useAddPageNumbersOperation.ts b/frontend/editor/src/core/components/tools/addPageNumbers/useAddPageNumbersOperation.ts index ba8ca13b32..eb1ba6def6 100644 --- a/frontend/editor/src/core/components/tools/addPageNumbers/useAddPageNumbersOperation.ts +++ b/frontend/editor/src/core/components/tools/addPageNumbers/useAddPageNumbersOperation.ts @@ -12,6 +12,7 @@ import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { AddPageNumbersParameters, defaultParameters, + validateAddPageNumbersParameters, } from "@app/components/tools/addPageNumbers/useAddPageNumbersParameters"; const ENDPOINT = "/api/v1/misc/add-page-numbers" satisfies ToolEndpoint; @@ -74,6 +75,7 @@ export const buildAddPageNumbersFormData = ( objectToFormData(addPageNumbersToApiParams(parameters), { fileInput: file }); export const addPageNumbersOperationConfig = defineSingleFileTool({ + validateParams: validateAddPageNumbersParameters, buildFormData: buildAddPageNumbersFormData, toApiParams: addPageNumbersToApiParams, fromApiParams: addPageNumbersFromApiParams, diff --git a/frontend/editor/src/core/components/tools/addPageNumbers/useAddPageNumbersParameters.ts b/frontend/editor/src/core/components/tools/addPageNumbers/useAddPageNumbersParameters.ts index 43f2d18ec0..5eae2f74e0 100644 --- a/frontend/editor/src/core/components/tools/addPageNumbers/useAddPageNumbersParameters.ts +++ b/frontend/editor/src/core/components/tools/addPageNumbers/useAddPageNumbersParameters.ts @@ -30,12 +30,16 @@ export const defaultParameters: AddPageNumbersParameters = { export type AddPageNumbersParametersHook = BaseParametersHook; +export function validateAddPageNumbersParameters( + params: AddPageNumbersParameters, +): boolean { + return params.fontSize > 0 && params.startingNumber > 0; +} + export const useAddPageNumbersParameters = (): AddPageNumbersParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "add-page-numbers", - validateFn: (params): boolean => { - return params.fontSize > 0 && params.startingNumber > 0; - }, + validateFn: validateAddPageNumbersParameters, }); }; diff --git a/frontend/editor/src/core/components/tools/addStamp/useAddStampOperation.ts b/frontend/editor/src/core/components/tools/addStamp/useAddStampOperation.ts index 71510694b0..69122754b7 100644 --- a/frontend/editor/src/core/components/tools/addStamp/useAddStampOperation.ts +++ b/frontend/editor/src/core/components/tools/addStamp/useAddStampOperation.ts @@ -12,6 +12,7 @@ import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { AddStampParameters, defaultParameters, + validateAddStampParameters, } from "@app/components/tools/addStamp/useAddStampParameters"; const ENDPOINT = "/api/v1/misc/add-stamp" satisfies ToolEndpoint; @@ -88,6 +89,7 @@ export const buildAddStampFormData = ( ); export const addStampOperationConfig = defineSingleFileTool({ + validateParams: validateAddStampParameters, buildFormData: buildAddStampFormData, toApiParams: addStampToApiParams, fromApiParams: addStampFromApiParams, diff --git a/frontend/editor/src/core/components/tools/addStamp/useAddStampParameters.ts b/frontend/editor/src/core/components/tools/addStamp/useAddStampParameters.ts index 637f4c0566..6cd52eb835 100644 --- a/frontend/editor/src/core/components/tools/addStamp/useAddStampParameters.ts +++ b/frontend/editor/src/core/components/tools/addStamp/useAddStampParameters.ts @@ -39,16 +39,20 @@ export const defaultParameters: AddStampParameters = { export type AddStampParametersHook = BaseParametersHook; +export function validateAddStampParameters( + params: AddStampParameters, +): boolean { + if (!params.stampType) return false; + if (params.stampType === "text") { + return params.stampText.trim().length > 0; + } + return params.stampImage !== undefined; +} + export const useAddStampParameters = (): AddStampParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "add-stamp", - validateFn: (params): boolean => { - if (!params.stampType) return false; - if (params.stampType === "text") { - return params.stampText.trim().length > 0; - } - return params.stampImage !== undefined; - }, + validateFn: validateAddStampParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordOperation.ts b/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordOperation.ts index f6eca6ffbd..d68154c9b1 100644 --- a/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordOperation.ts +++ b/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordOperation.ts @@ -10,6 +10,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateAddPasswordParameters, AddPasswordFullParameters, defaultParameters, } from "@app/hooks/tools/addPassword/useAddPasswordParameters"; @@ -86,6 +87,7 @@ const fullDefaultParameters: AddPasswordFullParameters = { // Static configuration object export const addPasswordOperationConfig = defineSingleFileTool({ + validateParams: validateAddPasswordParameters, buildFormData: buildAddPasswordFormData, toApiParams: addPasswordToApiParams, fromApiParams: addPasswordFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordParameters.ts b/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordParameters.ts index 25e68f7e6a..97f0b41aa8 100644 --- a/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordParameters.ts +++ b/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordParameters.ts @@ -2,6 +2,7 @@ import { ChangePermissionsParameters, ChangePermissionsParametersHook, useChangePermissionsParameters, + validateChangePermissionsParameters, } from "@app/hooks/tools/changePermissions/useChangePermissionsParameters"; import { BaseParameters } from "@app/types/parameters"; import { @@ -30,6 +31,16 @@ export const defaultParameters: AddPasswordParameters = { keyLength: 128, }; +/** + * Whether these parameters are complete enough to run. + * Add Password requires nothing of its own, so delegate to Change Permissions. + */ +export function validateAddPasswordParameters( + params: AddPasswordFullParameters, +): boolean { + return validateChangePermissionsParameters(params.permissions); +} + export const useAddPasswordParameters = (): AddPasswordParametersHook => { const permissions = useChangePermissionsParameters(); diff --git a/frontend/editor/src/core/hooks/tools/addWatermark/useAddWatermarkOperation.ts b/frontend/editor/src/core/hooks/tools/addWatermark/useAddWatermarkOperation.ts index 423110a2d2..b9f23a39bc 100644 --- a/frontend/editor/src/core/hooks/tools/addWatermark/useAddWatermarkOperation.ts +++ b/frontend/editor/src/core/hooks/tools/addWatermark/useAddWatermarkOperation.ts @@ -10,6 +10,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateAddWatermarkParameters, AddWatermarkParameters, defaultParameters, } from "@app/hooks/tools/addWatermark/useAddWatermarkParameters"; @@ -88,6 +89,7 @@ export const buildAddWatermarkFormData = ( // Static configuration object export const addWatermarkOperationConfig = defineSingleFileTool({ + validateParams: validateAddWatermarkParameters, buildFormData: buildAddWatermarkFormData, toApiParams: addWatermarkToApiParams, fromApiParams: addWatermarkFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/addWatermark/useAddWatermarkParameters.ts b/frontend/editor/src/core/hooks/tools/addWatermark/useAddWatermarkParameters.ts index 72f29e840b..83ea860193 100644 --- a/frontend/editor/src/core/hooks/tools/addWatermark/useAddWatermarkParameters.ts +++ b/frontend/editor/src/core/hooks/tools/addWatermark/useAddWatermarkParameters.ts @@ -34,19 +34,25 @@ export const defaultParameters: AddWatermarkParameters = { export type AddWatermarkParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateAddWatermarkParameters( + params: AddWatermarkParameters, +): boolean { + if (!params.watermarkType) { + return false; + } + if (params.watermarkType === "text") { + return params.watermarkText.trim().length > 0; + } else { + return params.watermarkImage !== undefined; + } +} + export const useAddWatermarkParameters = (): AddWatermarkParametersHook => { return useBaseParameters({ defaultParameters: defaultParameters, endpointName: "add-watermark", - validateFn: (params): boolean => { - if (!params.watermarkType) { - return false; - } - if (params.watermarkType === "text") { - return params.watermarkText.trim().length > 0; - } else { - return params.watermarkImage !== undefined; - } - }, + validateFn: validateAddWatermarkParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/adjustPageScale/useAdjustPageScaleOperation.ts b/frontend/editor/src/core/hooks/tools/adjustPageScale/useAdjustPageScaleOperation.ts index db0ce22da2..9a52cba20d 100644 --- a/frontend/editor/src/core/hooks/tools/adjustPageScale/useAdjustPageScaleOperation.ts +++ b/frontend/editor/src/core/hooks/tools/adjustPageScale/useAdjustPageScaleOperation.ts @@ -5,6 +5,7 @@ import { } from "@app/hooks/tools/shared/useToolOperation"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateAdjustPageScaleParameters, AdjustPageScaleParameters, defaultParameters, } from "@app/hooks/tools/adjustPageScale/useAdjustPageScaleParameters"; @@ -22,6 +23,7 @@ export { }; export const adjustPageScaleOperationConfig = defineSingleFileTool({ + validateParams: validateAdjustPageScaleParameters, buildFormData: buildAdjustPageScaleFormData, toApiParams: adjustPageScaleToApiParams, fromApiParams: adjustPageScaleFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/adjustPageScale/useAdjustPageScaleParameters.ts b/frontend/editor/src/core/hooks/tools/adjustPageScale/useAdjustPageScaleParameters.ts index b099bcc39b..09b1be0e6f 100644 --- a/frontend/editor/src/core/hooks/tools/adjustPageScale/useAdjustPageScaleParameters.ts +++ b/frontend/editor/src/core/hooks/tools/adjustPageScale/useAdjustPageScaleParameters.ts @@ -34,13 +34,19 @@ export const defaultParameters: AdjustPageScaleParameters = { export type AdjustPageScaleParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateAdjustPageScaleParameters( + params: AdjustPageScaleParameters, +): boolean { + return params.scaleFactor > 0; +} + export const useAdjustPageScaleParameters = (): AdjustPageScaleParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "scale-pages", - validateFn: (params) => { - return params.scaleFactor > 0; - }, + validateFn: validateAdjustPageScaleParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/autoRotate/useAutoRotateOperation.ts b/frontend/editor/src/core/hooks/tools/autoRotate/useAutoRotateOperation.ts index 488a15a8e2..fde4ae1eca 100644 --- a/frontend/editor/src/core/hooks/tools/autoRotate/useAutoRotateOperation.ts +++ b/frontend/editor/src/core/hooks/tools/autoRotate/useAutoRotateOperation.ts @@ -24,6 +24,7 @@ import { AutoRotateParameters, AutoRotateDetectionMode, defaultParameters, + validateAutoRotateParameters, } from "@app/hooks/tools/autoRotate/useAutoRotateParameters"; export const AUTO_ROTATE_ENDPOINT = @@ -169,6 +170,7 @@ export const autoRotateOperationConfig = defineCustomTool( toApiParams: autoRotateToApiParams, fromApiParams: autoRotateFromApiParams, defaultParameters, + validateParams: validateAutoRotateParameters, }, ); diff --git a/frontend/editor/src/core/hooks/tools/autoRotate/useAutoRotateParameters.ts b/frontend/editor/src/core/hooks/tools/autoRotate/useAutoRotateParameters.ts index fd61395eb1..4c4d318ba6 100644 --- a/frontend/editor/src/core/hooks/tools/autoRotate/useAutoRotateParameters.ts +++ b/frontend/editor/src/core/hooks/tools/autoRotate/useAutoRotateParameters.ts @@ -23,9 +23,15 @@ export const defaultParameters: AutoRotateParameters = { export type AutoRotateParametersHook = BaseParametersHook; +export function validateAutoRotateParameters( + params: AutoRotateParameters, +): boolean { + return params.confidenceThreshold >= 0; +} + export const useAutoRotateParameters = (): AutoRotateParametersHook => useBaseParameters({ defaultParameters, endpointName: "auto-rotate-pdf", - validateFn: (params) => params.confidenceThreshold >= 0, + validateFn: validateAutoRotateParameters, }); diff --git a/frontend/editor/src/core/hooks/tools/bookletImposition/useBookletImpositionOperation.ts b/frontend/editor/src/core/hooks/tools/bookletImposition/useBookletImpositionOperation.ts index 7add5d126c..d31e1506cf 100644 --- a/frontend/editor/src/core/hooks/tools/bookletImposition/useBookletImpositionOperation.ts +++ b/frontend/editor/src/core/hooks/tools/bookletImposition/useBookletImpositionOperation.ts @@ -10,6 +10,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateBookletImpositionParameters, BookletImpositionParameters, defaultParameters, } from "@app/hooks/tools/bookletImposition/useBookletImpositionParameters"; @@ -60,6 +61,7 @@ export const buildBookletImpositionFormData = ( // Static configuration object export const bookletImpositionOperationConfig = defineSingleFileTool({ + validateParams: validateBookletImpositionParameters, buildFormData: buildBookletImpositionFormData, toApiParams: bookletImpositionToApiParams, fromApiParams: bookletImpositionFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/bookletImposition/useBookletImpositionParameters.ts b/frontend/editor/src/core/hooks/tools/bookletImposition/useBookletImpositionParameters.ts index 1c90b8ba9e..fa0706d255 100644 --- a/frontend/editor/src/core/hooks/tools/bookletImposition/useBookletImpositionParameters.ts +++ b/frontend/editor/src/core/hooks/tools/bookletImposition/useBookletImpositionParameters.ts @@ -29,13 +29,19 @@ export const defaultParameters: BookletImpositionParameters = { export type BookletImpositionParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateBookletImpositionParameters( + params: BookletImpositionParameters, +): boolean { + return params.pagesPerSheet === 2; +} + export const useBookletImpositionParameters = (): BookletImpositionParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "booklet-imposition", - validateFn: (params) => { - return params.pagesPerSheet === 2; - }, + validateFn: validateBookletImpositionParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/certSign/useCertSignOperation.ts b/frontend/editor/src/core/hooks/tools/certSign/useCertSignOperation.ts index 01cfa32b1a..22a89cdb0a 100644 --- a/frontend/editor/src/core/hooks/tools/certSign/useCertSignOperation.ts +++ b/frontend/editor/src/core/hooks/tools/certSign/useCertSignOperation.ts @@ -11,6 +11,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateCertSignParameters, CertSignParameters, defaultParameters, } from "@app/hooks/tools/certSign/useCertSignParameters"; @@ -137,6 +138,7 @@ export const buildCertSignFormData = ( // Static configuration object export const certSignOperationConfig = defineSingleFileTool({ + validateParams: validateCertSignParameters, buildFormData: buildCertSignFormData, toApiParams: certSignToApiParams, fromApiParams: certSignFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/certSign/useCertSignParameters.ts b/frontend/editor/src/core/hooks/tools/certSign/useCertSignParameters.ts index f69c98ffd2..a1d50a4e2c 100644 --- a/frontend/editor/src/core/hooks/tools/certSign/useCertSignParameters.ts +++ b/frontend/editor/src/core/hooks/tools/certSign/useCertSignParameters.ts @@ -46,39 +46,45 @@ export const defaultParameters: CertSignParameters = { export type CertSignParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateCertSignParameters( + params: CertSignParameters, +): boolean { + // Auto mode (server certificate) - no additional validation needed + if (params.signMode === "AUTO") { + return true; + } + + // Manual mode - requires certificate type and files + if (!params.certType) { + return false; + } + + // Check for required files based on cert type + switch (params.certType) { + case "PEM": + return !!(params.privateKeyFile && params.certFile); + case "PKCS12": + case "PFX": + return !!params.p12File; + case "JKS": + return !!params.jksFile; + case "WINDOWS_STORE": + // Need a chosen certificate from the Windows store. + return !!params.alias; + case "PKCS11": + // Need a driver library and a chosen certificate on the token. + return !!(params.pkcs11LibraryPath && params.alias); + default: + return false; + } +} + export const useCertSignParameters = (): CertSignParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "cert-sign", - validateFn: (params) => { - // Auto mode (server certificate) - no additional validation needed - if (params.signMode === "AUTO") { - return true; - } - - // Manual mode - requires certificate type and files - if (!params.certType) { - return false; - } - - // Check for required files based on cert type - switch (params.certType) { - case "PEM": - return !!(params.privateKeyFile && params.certFile); - case "PKCS12": - case "PFX": - return !!params.p12File; - case "JKS": - return !!params.jksFile; - case "WINDOWS_STORE": - // Need a chosen certificate from the Windows store. - return !!params.alias; - case "PKCS11": - // Need a driver library and a chosen certificate on the token. - return !!(params.pkcs11LibraryPath && params.alias); - default: - return false; - } - }, + validateFn: validateCertSignParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/changeMetadata/useChangeMetadataOperation.ts b/frontend/editor/src/core/hooks/tools/changeMetadata/useChangeMetadataOperation.ts index 97a8285194..dffe83d8e9 100644 --- a/frontend/editor/src/core/hooks/tools/changeMetadata/useChangeMetadataOperation.ts +++ b/frontend/editor/src/core/hooks/tools/changeMetadata/useChangeMetadataOperation.ts @@ -11,6 +11,7 @@ import { import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { TrappedStatus, CustomMetadataEntry } from "@app/types/metadata"; import { + validateChangeMetadataParameters, ChangeMetadataParameters, defaultParameters, } from "@app/hooks/tools/changeMetadata/useChangeMetadataParameters"; @@ -154,6 +155,7 @@ export const buildChangeMetadataFormData = ( // Static configuration object export const changeMetadataOperationConfig = defineSingleFileTool({ + validateParams: validateChangeMetadataParameters, buildFormData: buildChangeMetadataFormData, toApiParams: changeMetadataToApiParams, fromApiParams: changeMetadataFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/changeMetadata/useChangeMetadataParameters.ts b/frontend/editor/src/core/hooks/tools/changeMetadata/useChangeMetadataParameters.ts index 4966f4b40f..83588e1619 100644 --- a/frontend/editor/src/core/hooks/tools/changeMetadata/useChangeMetadataParameters.ts +++ b/frontend/editor/src/core/hooks/tools/changeMetadata/useChangeMetadataParameters.ts @@ -90,7 +90,9 @@ export const createCustomMetadataFunctions = ( }; // Validation function -const validateParameters = (params: ChangeMetadataParameters): boolean => { +export const validateChangeMetadataParameters = ( + params: ChangeMetadataParameters, +): boolean => { // If deleteAll is true, no other validation needed if (params.deleteAll) { return true; @@ -127,7 +129,7 @@ export const useChangeMetadataParameters = (): ChangeMetadataParametersHook => { const base = useBaseParameters({ defaultParameters, endpointName: "update-metadata", - validateFn: validateParameters, + validateFn: validateChangeMetadataParameters, }); // Use the utility functions with the hook's parameters and updateParameter diff --git a/frontend/editor/src/core/hooks/tools/changePermissions/useChangePermissionsOperation.ts b/frontend/editor/src/core/hooks/tools/changePermissions/useChangePermissionsOperation.ts index a2e3eb3d1d..ac5f9a7d96 100644 --- a/frontend/editor/src/core/hooks/tools/changePermissions/useChangePermissionsOperation.ts +++ b/frontend/editor/src/core/hooks/tools/changePermissions/useChangePermissionsOperation.ts @@ -12,6 +12,7 @@ import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { ChangePermissionsParameters, defaultParameters, + validateChangePermissionsParameters, } from "@app/hooks/tools/changePermissions/useChangePermissionsParameters"; // Change Permissions reuses the Add Password endpoint but sends only the @@ -72,6 +73,7 @@ export const buildChangePermissionsFormData = ( // Static configuration object export const changePermissionsOperationConfig = defineSingleFileTool({ + validateParams: validateChangePermissionsParameters, buildFormData: buildChangePermissionsFormData, toApiParams: changePermissionsToApiParams, fromApiParams: changePermissionsFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/changePermissions/useChangePermissionsParameters.ts b/frontend/editor/src/core/hooks/tools/changePermissions/useChangePermissionsParameters.ts index 30220c13e3..039f7214fe 100644 --- a/frontend/editor/src/core/hooks/tools/changePermissions/useChangePermissionsParameters.ts +++ b/frontend/editor/src/core/hooks/tools/changePermissions/useChangePermissionsParameters.ts @@ -29,10 +29,21 @@ export const defaultParameters: ChangePermissionsParameters = { export type ChangePermissionsParametersHook = BaseParametersHook; +/** + * Whether these permissions are complete enough to run. + * Every field is a boolean with a default, so today any combination is a valid request. + */ +export function validateChangePermissionsParameters( + _params: ChangePermissionsParameters, +): boolean { + return true; +} + export const useChangePermissionsParameters = (): ChangePermissionsParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "add-password", // Change Permissions is a fake endpoint for the Add Password tool + validateFn: validateChangePermissionsParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/compare/useCompareParameters.ts b/frontend/editor/src/core/hooks/tools/compare/useCompareParameters.ts index 693f537fa6..1634e3fe27 100644 --- a/frontend/editor/src/core/hooks/tools/compare/useCompareParameters.ts +++ b/frontend/editor/src/core/hooks/tools/compare/useCompareParameters.ts @@ -26,15 +26,20 @@ export const defaultParameters: CompareParameters = { export type CompareParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings hook + * and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateCompareParameters(params: CompareParameters): boolean { + return Boolean( + params.baseFileId && + params.comparisonFileId && + params.baseFileId !== params.comparisonFileId, + ); +} + export const useCompareParameters = (): CompareParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "compare", - validateFn: (params) => - Boolean( - params.baseFileId && - params.comparisonFileId && - params.baseFileId !== params.comparisonFileId, - ), + validateFn: validateCompareParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/compress/useCompressOperation.ts b/frontend/editor/src/core/hooks/tools/compress/useCompressOperation.ts index b24bb8c6ee..1c21418dbb 100644 --- a/frontend/editor/src/core/hooks/tools/compress/useCompressOperation.ts +++ b/frontend/editor/src/core/hooks/tools/compress/useCompressOperation.ts @@ -10,6 +10,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateCompressParameters, CompressParameters, defaultParameters, } from "@app/hooks/tools/compress/useCompressParameters"; @@ -91,6 +92,7 @@ export const buildCompressFormData = ( // Static configuration object export const compressOperationConfig = defineSingleFileTool({ + validateParams: validateCompressParameters, buildFormData: buildCompressFormData, toApiParams: compressToApiParams, fromApiParams: compressFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/compress/useCompressParameters.ts b/frontend/editor/src/core/hooks/tools/compress/useCompressParameters.ts index 0500b8c77c..b27a9e97cc 100644 --- a/frontend/editor/src/core/hooks/tools/compress/useCompressParameters.ts +++ b/frontend/editor/src/core/hooks/tools/compress/useCompressParameters.ts @@ -32,20 +32,26 @@ export const defaultParameters: CompressParameters = { export type CompressParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateCompressParameters( + params: CompressParameters, +): boolean { + if (params.compressionLevel < 1 || params.compressionLevel > 9) { + return false; + } + // Filesize mode needs a target size; without one the request omits + // expectedOutputSize and the backend silently does a quality compression. + if (params.compressionMethod === "filesize") { + return params.fileSizeValue.trim() !== ""; + } + return true; +} + export const useCompressParameters = (): CompressParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "compress-pdf", - validateFn: (params) => { - if (params.compressionLevel < 1 || params.compressionLevel > 9) { - return false; - } - // Filesize mode needs a target size; without one the request omits - // expectedOutputSize and the backend silently does a quality compression. - if (params.compressionMethod === "filesize") { - return params.fileSizeValue.trim() !== ""; - } - return true; - }, + validateFn: validateCompressParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/convert/useConvertOperation.ts b/frontend/editor/src/core/hooks/tools/convert/useConvertOperation.ts index a9488e373d..d6175e2b8b 100644 --- a/frontend/editor/src/core/hooks/tools/convert/useConvertOperation.ts +++ b/frontend/editor/src/core/hooks/tools/convert/useConvertOperation.ts @@ -2,6 +2,7 @@ import { useCallback, useMemo } from "react"; import apiClient from "@app/services/apiClient"; import { useTranslation } from "react-i18next"; import { + validateConvertParameters, ConvertParameters, defaultParameters, } from "@app/hooks/tools/convert/useConvertParameters"; @@ -301,6 +302,7 @@ export const convertProcessor = async ( // Static configuration object export const convertOperationConfig = defineCustomTool({ + validateParams: validateConvertParameters, customProcessor: convertProcessor, // Can't use callback version here operationType: "convert", defaultParameters, diff --git a/frontend/editor/src/core/hooks/tools/convert/useConvertParameters.ts b/frontend/editor/src/core/hooks/tools/convert/useConvertParameters.ts index c2e3ba2e1a..b1c0151cc8 100644 --- a/frontend/editor/src/core/hooks/tools/convert/useConvertParameters.ts +++ b/frontend/editor/src/core/hooks/tools/convert/useConvertParameters.ts @@ -138,7 +138,9 @@ export const defaultParameters: ConvertParameters = { smartDetectionType: "none", }; -const validateParameters = (params: ConvertParameters): boolean => { +export const validateConvertParameters = ( + params: ConvertParameters, +): boolean => { const { fromExtension, toExtension } = params; if (!fromExtension || !toExtension) return false; @@ -191,7 +193,7 @@ export const useConvertParameters = (): ConvertParametersHook => { () => ({ defaultParameters, endpointName: getEndpointName, - validateFn: validateParameters, + validateFn: validateConvertParameters, }), [], ); diff --git a/frontend/editor/src/core/hooks/tools/crop/useCropOperation.ts b/frontend/editor/src/core/hooks/tools/crop/useCropOperation.ts index b3a69f9dbe..287980ec03 100644 --- a/frontend/editor/src/core/hooks/tools/crop/useCropOperation.ts +++ b/frontend/editor/src/core/hooks/tools/crop/useCropOperation.ts @@ -10,6 +10,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateCropParameters, CropParameters, defaultParameters, } from "@app/hooks/tools/crop/useCropParameters"; @@ -60,6 +61,7 @@ export const buildCropFormData = ( // Static configuration object export const cropOperationConfig = defineSingleFileTool({ + validateParams: validateCropParameters, buildFormData: buildCropFormData, toApiParams: cropToApiParams, fromApiParams: cropFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/crop/useCropParameters.ts b/frontend/editor/src/core/hooks/tools/crop/useCropParameters.ts index 920f2cb6c5..cc37421cdf 100644 --- a/frontend/editor/src/core/hooks/tools/crop/useCropParameters.ts +++ b/frontend/editor/src/core/hooks/tools/crop/useCropParameters.ts @@ -42,15 +42,19 @@ export type CropParametersHook = BaseParametersHook & { ) => void; }; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateCropParameters(params: CropParameters): boolean { + const rect = params.cropArea; + // Basic validation - coordinates and dimensions must be positive + return rect.x >= 0 && rect.y >= 0 && rect.width > 0 && rect.height > 0; +} + export const useCropParameters = (): CropParametersHook => { const baseHook = useBaseParameters({ defaultParameters, endpointName: "crop", - validateFn: (params) => { - const rect = params.cropArea; - // Basic validation - coordinates and dimensions must be positive - return rect.x >= 0 && rect.y >= 0 && rect.width > 0 && rect.height > 0; - }, + validateFn: validateCropParameters, }); // Get current crop area as CropArea object diff --git a/frontend/editor/src/core/hooks/tools/extractPages/useExtractPagesOperation.ts b/frontend/editor/src/core/hooks/tools/extractPages/useExtractPagesOperation.ts index 0cd5273f0b..67ddb52594 100644 --- a/frontend/editor/src/core/hooks/tools/extractPages/useExtractPagesOperation.ts +++ b/frontend/editor/src/core/hooks/tools/extractPages/useExtractPagesOperation.ts @@ -7,6 +7,7 @@ import { } from "@app/hooks/tools/shared/useToolOperation"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateExtractPagesParameters, ExtractPagesParameters, defaultParameters, } from "@app/hooks/tools/extractPages/useExtractPagesParameters"; @@ -34,6 +35,7 @@ async function resolveSelectionToCsv( } export const extractPagesOperationConfig = defineCustomTool({ + validateParams: validateExtractPagesParameters, operationType: "extractPages", customProcessor: async ( parameters: ExtractPagesParameters, diff --git a/frontend/editor/src/core/hooks/tools/extractPages/useExtractPagesParameters.ts b/frontend/editor/src/core/hooks/tools/extractPages/useExtractPagesParameters.ts index 60bb2b5848..24daacce65 100644 --- a/frontend/editor/src/core/hooks/tools/extractPages/useExtractPagesParameters.ts +++ b/frontend/editor/src/core/hooks/tools/extractPages/useExtractPagesParameters.ts @@ -15,10 +15,18 @@ export const defaultParameters: ExtractPagesParameters = { export type ExtractPagesParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings hook + * and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateExtractPagesParameters( + params: ExtractPagesParameters, +): boolean { + return (params.pageNumbers || "").trim().length > 0; +} + export const useExtractPagesParameters = (): ExtractPagesParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "rearrange-pages", - validateFn: (p) => (p.pageNumbers || "").trim().length > 0, + validateFn: validateExtractPagesParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/ocr/useOCROperation.ts b/frontend/editor/src/core/hooks/tools/ocr/useOCROperation.ts index f995ef06fc..79b339dda9 100644 --- a/frontend/editor/src/core/hooks/tools/ocr/useOCROperation.ts +++ b/frontend/editor/src/core/hooks/tools/ocr/useOCROperation.ts @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { OCRParameters, defaultParameters, + validateOCRParameters, } from "@app/hooks/tools/ocr/useOCRParameters"; import { useToolOperation, @@ -152,6 +153,7 @@ export const ocrResponseHandler = async ( // Static configuration object (without t function dependencies) export const ocrOperationConfig = defineSingleFileTool({ + validateParams: validateOCRParameters, buildFormData: buildOCRFormData, toApiParams: ocrToApiParams, fromApiParams: ocrFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/ocr/useOCRParameters.ts b/frontend/editor/src/core/hooks/tools/ocr/useOCRParameters.ts index a8bd5abb8d..8f7b0f83ee 100644 --- a/frontend/editor/src/core/hooks/tools/ocr/useOCRParameters.ts +++ b/frontend/editor/src/core/hooks/tools/ocr/useOCRParameters.ts @@ -20,13 +20,17 @@ export const defaultParameters: OCRParameters = { additionalOptions: [], }; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateOCRParameters(params: OCRParameters): boolean { + // At minimum, we need at least one language selected + return params.languages.length > 0; +} + export const useOCRParameters = (): OCRParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "ocr-pdf", - validateFn: (params) => { - // At minimum, we need at least one language selected - return params.languages.length > 0; - }, + validateFn: validateOCRParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/overlayPdfs/useOverlayPdfsOperation.ts b/frontend/editor/src/core/hooks/tools/overlayPdfs/useOverlayPdfsOperation.ts index f71c237e9d..855d921b06 100644 --- a/frontend/editor/src/core/hooks/tools/overlayPdfs/useOverlayPdfsOperation.ts +++ b/frontend/editor/src/core/hooks/tools/overlayPdfs/useOverlayPdfsOperation.ts @@ -11,6 +11,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateOverlayPdfsParameters, type OverlayPdfsParameters, defaultParameters, } from "@app/hooks/tools/overlayPdfs/useOverlayPdfsParameters"; @@ -61,6 +62,7 @@ const buildFormData = ( export const overlayPdfsOperationConfig: ToolOperationConfig = defineSingleFileTool({ + validateParams: validateOverlayPdfsParameters, buildFormData, toApiParams: overlayPdfsToApiParams, fromApiParams: overlayPdfsFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/overlayPdfs/useOverlayPdfsParameters.ts b/frontend/editor/src/core/hooks/tools/overlayPdfs/useOverlayPdfsParameters.ts index fc88ad4ef0..f2f892f172 100644 --- a/frontend/editor/src/core/hooks/tools/overlayPdfs/useOverlayPdfsParameters.ts +++ b/frontend/editor/src/core/hooks/tools/overlayPdfs/useOverlayPdfsParameters.ts @@ -27,24 +27,25 @@ export const defaultParameters: OverlayPdfsParameters = { export type OverlayPdfsParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateOverlayPdfsParameters( + params: OverlayPdfsParameters, +): boolean { + if (!params.overlayFiles || params.overlayFiles.length === 0) return false; + if (params.overlayMode === "FixedRepeatOverlay") { + if (!params.counts || params.counts.length !== params.overlayFiles.length) + return false; + if (params.counts.some((c) => !Number.isFinite(c) || c <= 0)) return false; + } + return true; +} + export const useOverlayPdfsParameters = (): OverlayPdfsParametersHook => { const base = useBaseParameters({ defaultParameters, endpointName: "overlay-pdfs", - validateFn: (params) => { - if (!params.overlayFiles || params.overlayFiles.length === 0) - return false; - if (params.overlayMode === "FixedRepeatOverlay") { - if ( - !params.counts || - params.counts.length !== params.overlayFiles.length - ) - return false; - if (params.counts.some((c) => !Number.isFinite(c) || c <= 0)) - return false; - } - return true; - }, + validateFn: validateOverlayPdfsParameters, }); // Overlay files are chosen independently of the base file selection, so they diff --git a/frontend/editor/src/core/hooks/tools/pageLayout/usePageLayoutOperation.ts b/frontend/editor/src/core/hooks/tools/pageLayout/usePageLayoutOperation.ts index 5de635f965..3f06456702 100644 --- a/frontend/editor/src/core/hooks/tools/pageLayout/usePageLayoutOperation.ts +++ b/frontend/editor/src/core/hooks/tools/pageLayout/usePageLayoutOperation.ts @@ -10,6 +10,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validatePageLayoutParameters, PageLayoutParameters, defaultParameters, } from "@app/hooks/tools/pageLayout/usePageLayoutParameters"; @@ -68,6 +69,7 @@ export const buildPageLayoutFormData = ( objectToFormData(pageLayoutToApiParams(parameters), { fileInput: file }); export const pageLayoutOperationConfig = defineSingleFileTool({ + validateParams: validatePageLayoutParameters, buildFormData: buildPageLayoutFormData, toApiParams: pageLayoutToApiParams, fromApiParams: pageLayoutFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/pageLayout/usePageLayoutParameters.ts b/frontend/editor/src/core/hooks/tools/pageLayout/usePageLayoutParameters.ts index 89ae157ef8..3aeb69288f 100644 --- a/frontend/editor/src/core/hooks/tools/pageLayout/usePageLayoutParameters.ts +++ b/frontend/editor/src/core/hooks/tools/pageLayout/usePageLayoutParameters.ts @@ -40,41 +40,47 @@ export const defaultParameters: PageLayoutParameters = { export type PageLayoutParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validatePageLayoutParameters( + params: PageLayoutParameters, +): boolean { + const cols = + params.mode === "DEFAULT" + ? Math.ceil(Math.sqrt(params.pagesPerSheet)) + : params.cols; + const rows = + params.mode === "DEFAULT" + ? Math.ceil(params.pagesPerSheet / cols) + : params.rows; + + if (cols <= 0 || rows <= 0) return false; + + const pageWidth = params.orientation === "PORTRAIT" ? 595.28 : 841.89; + const pageHeight = params.orientation === "PORTRAIT" ? 841.89 : 595.28; + + const left = params.leftMargin ?? 0; + const right = params.rightMargin ?? 0; + const top = params.topMargin ?? 0; + const bottom = params.bottomMargin ?? 0; + const inner = params.innerMargin ?? 0; + + // Reject impossible outer margins first. + if (left + right >= pageWidth) return false; + if (top + bottom >= pageHeight) return false; + + const cellWidth = (pageWidth - left - right) / cols; + const cellHeight = (pageHeight - top - bottom) / rows; + const innerWidth = cellWidth - 2 * inner; + const innerHeight = cellHeight - 2 * inner; + + return innerWidth > 0 && innerHeight > 0; +} + export const usePageLayoutParameters = (): PageLayoutParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "multi-page-layout", - validateFn: (params) => { - const cols = - params.mode === "DEFAULT" - ? Math.ceil(Math.sqrt(params.pagesPerSheet)) - : params.cols; - const rows = - params.mode === "DEFAULT" - ? Math.ceil(params.pagesPerSheet / cols) - : params.rows; - - if (cols <= 0 || rows <= 0) return false; - - const pageWidth = params.orientation === "PORTRAIT" ? 595.28 : 841.89; - const pageHeight = params.orientation === "PORTRAIT" ? 841.89 : 595.28; - - const left = params.leftMargin ?? 0; - const right = params.rightMargin ?? 0; - const top = params.topMargin ?? 0; - const bottom = params.bottomMargin ?? 0; - const inner = params.innerMargin ?? 0; - - // Reject impossible outer margins first. - if (left + right >= pageWidth) return false; - if (top + bottom >= pageHeight) return false; - - const cellWidth = (pageWidth - left - right) / cols; - const cellHeight = (pageHeight - top - bottom) / rows; - const innerWidth = cellWidth - 2 * inner; - const innerHeight = cellHeight - 2 * inner; - - return innerWidth > 0 && innerHeight > 0; - }, + validateFn: validatePageLayoutParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/redact/useRedactOperation.ts b/frontend/editor/src/core/hooks/tools/redact/useRedactOperation.ts index 82d45e9e55..fbfa5045e4 100644 --- a/frontend/editor/src/core/hooks/tools/redact/useRedactOperation.ts +++ b/frontend/editor/src/core/hooks/tools/redact/useRedactOperation.ts @@ -10,6 +10,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateRedactParameters, RedactParameters, defaultParameters, } from "@app/hooks/tools/redact/useRedactParameters"; @@ -65,6 +66,7 @@ export const buildRedactFormData = ( // Static configuration object export const redactOperationConfig = defineSingleFileTool({ + validateParams: validateRedactParameters, buildFormData: buildRedactFormData, toApiParams: redactToApiParams, fromApiParams: redactFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/redact/useRedactParameters.ts b/frontend/editor/src/core/hooks/tools/redact/useRedactParameters.ts index e41b9d2216..048b230567 100644 --- a/frontend/editor/src/core/hooks/tools/redact/useRedactParameters.ts +++ b/frontend/editor/src/core/hooks/tools/redact/useRedactParameters.ts @@ -30,6 +30,19 @@ export const defaultParameters: RedactParameters = { export type RedactParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateRedactParameters(params: RedactParameters): boolean { + if (params.mode === "automatic") { + return ( + params.wordsToRedact.length > 0 && + params.wordsToRedact.some((word) => word.trim().length > 0) + ); + } + // Manual mode is not yet supported via this flow + return false; +} + export const useRedactParameters = (): RedactParametersHook => { return useBaseParameters({ defaultParameters, @@ -40,15 +53,6 @@ export const useRedactParameters = (): RedactParametersHook => { // Manual redaction handled client-side (validation prevents this path) return ""; }, - validateFn: (params) => { - if (params.mode === "automatic") { - return ( - params.wordsToRedact.length > 0 && - params.wordsToRedact.some((word) => word.trim().length > 0) - ); - } - // Manual mode is not yet supported via this flow - return false; - }, + validateFn: validateRedactParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/removeBlanks/useRemoveBlanksOperation.ts b/frontend/editor/src/core/hooks/tools/removeBlanks/useRemoveBlanksOperation.ts index 54e45d43fe..46366508b4 100644 --- a/frontend/editor/src/core/hooks/tools/removeBlanks/useRemoveBlanksOperation.ts +++ b/frontend/editor/src/core/hooks/tools/removeBlanks/useRemoveBlanksOperation.ts @@ -11,6 +11,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateRemoveBlanksParameters, RemoveBlanksParameters, defaultParameters, } from "@app/hooks/tools/removeBlanks/useRemoveBlanksParameters"; @@ -41,6 +42,7 @@ export const buildRemoveBlanksFormData = ( objectToFormData(removeBlanksToApiParams(parameters), { fileInput: file }); export const removeBlanksOperationConfig = defineSingleFileTool({ + validateParams: validateRemoveBlanksParameters, buildFormData: buildRemoveBlanksFormData, toApiParams: removeBlanksToApiParams, fromApiParams: removeBlanksFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/removeBlanks/useRemoveBlanksParameters.ts b/frontend/editor/src/core/hooks/tools/removeBlanks/useRemoveBlanksParameters.ts index 2eb5c9bce5..292807128f 100644 --- a/frontend/editor/src/core/hooks/tools/removeBlanks/useRemoveBlanksParameters.ts +++ b/frontend/editor/src/core/hooks/tools/removeBlanks/useRemoveBlanksParameters.ts @@ -19,14 +19,23 @@ export const defaultParameters: RemoveBlanksParameters = { export type RemoveBlanksParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings hook + * and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateRemoveBlanksParameters( + params: RemoveBlanksParameters, +): boolean { + return ( + params.threshold >= 0 && + params.threshold <= 255 && + params.whitePercent > 0 && + params.whitePercent <= 100 + ); +} + export const useRemoveBlanksParameters = (): RemoveBlanksParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "remove-blanks", - validateFn: (p) => - p.threshold >= 0 && - p.threshold <= 255 && - p.whitePercent > 0 && - p.whitePercent <= 100, + validateFn: validateRemoveBlanksParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/removePages/useRemovePagesOperation.ts b/frontend/editor/src/core/hooks/tools/removePages/useRemovePagesOperation.ts index c8a8aca2d0..b44db4d5c4 100644 --- a/frontend/editor/src/core/hooks/tools/removePages/useRemovePagesOperation.ts +++ b/frontend/editor/src/core/hooks/tools/removePages/useRemovePagesOperation.ts @@ -10,6 +10,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateRemovePagesParameters, RemovePagesParameters, defaultParameters, } from "@app/hooks/tools/removePages/useRemovePagesParameters"; @@ -42,6 +43,7 @@ export const buildRemovePagesFormData = ( objectToFormData(removePagesToApiParams(parameters), { fileInput: file }); export const removePagesOperationConfig = defineSingleFileTool({ + validateParams: validateRemovePagesParameters, buildFormData: buildRemovePagesFormData, toApiParams: removePagesToApiParams, fromApiParams: removePagesFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/removePages/useRemovePagesParameters.ts b/frontend/editor/src/core/hooks/tools/removePages/useRemovePagesParameters.ts index 97a4953203..3995be8571 100644 --- a/frontend/editor/src/core/hooks/tools/removePages/useRemovePagesParameters.ts +++ b/frontend/editor/src/core/hooks/tools/removePages/useRemovePagesParameters.ts @@ -16,10 +16,18 @@ export const defaultParameters: RemovePagesParameters = { export type RemovePagesParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings hook + * and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateRemovePagesParameters( + params: RemovePagesParameters, +): boolean { + return validatePageNumbers(params.pageNumbers); +} + export const useRemovePagesParameters = (): RemovePagesParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "remove-pages", - validateFn: (p) => validatePageNumbers(p.pageNumbers), + validateFn: validateRemovePagesParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/removePassword/useRemovePasswordOperation.ts b/frontend/editor/src/core/hooks/tools/removePassword/useRemovePasswordOperation.ts index 644bbe3b1a..e8448bf518 100644 --- a/frontend/editor/src/core/hooks/tools/removePassword/useRemovePasswordOperation.ts +++ b/frontend/editor/src/core/hooks/tools/removePassword/useRemovePasswordOperation.ts @@ -5,6 +5,7 @@ import { } from "@app/hooks/tools/shared/useToolOperation"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateRemovePasswordParameters, RemovePasswordParameters, defaultParameters, } from "@app/hooks/tools/removePassword/useRemovePasswordParameters"; @@ -20,6 +21,7 @@ export { buildRemovePasswordFormData }; // Static configuration object export const removePasswordOperationConfig = defineSingleFileTool({ + validateParams: validateRemovePasswordParameters, buildFormData: buildRemovePasswordFormData, toApiParams: removePasswordToApiParams, fromApiParams: removePasswordFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/removePassword/useRemovePasswordParameters.ts b/frontend/editor/src/core/hooks/tools/removePassword/useRemovePasswordParameters.ts index ebb0dd317f..af920a20f2 100644 --- a/frontend/editor/src/core/hooks/tools/removePassword/useRemovePasswordParameters.ts +++ b/frontend/editor/src/core/hooks/tools/removePassword/useRemovePasswordParameters.ts @@ -15,12 +15,18 @@ export const defaultParameters: RemovePasswordParameters = { password: "", }; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateRemovePasswordParameters( + params: RemovePasswordParameters, +): boolean { + return params.password !== ""; +} + export const useRemovePasswordParameters = (): RemovePasswordParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "remove-password", - validateFn: (params) => { - return params.password !== ""; - }, + validateFn: validateRemovePasswordParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/replaceColor/useReplaceColorOperation.ts b/frontend/editor/src/core/hooks/tools/replaceColor/useReplaceColorOperation.ts index 33223afa43..3b54ec8827 100644 --- a/frontend/editor/src/core/hooks/tools/replaceColor/useReplaceColorOperation.ts +++ b/frontend/editor/src/core/hooks/tools/replaceColor/useReplaceColorOperation.ts @@ -10,6 +10,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateReplaceColorParameters, ReplaceColorParameters, defaultParameters, } from "@app/hooks/tools/replaceColor/useReplaceColorParameters"; @@ -63,6 +64,7 @@ export const buildReplaceColorFormData = ( objectToFormData(replaceColorToApiParams(parameters), { fileInput: file }); export const replaceColorOperationConfig = defineSingleFileTool({ + validateParams: validateReplaceColorParameters, buildFormData: buildReplaceColorFormData, toApiParams: replaceColorToApiParams, fromApiParams: replaceColorFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/replaceColor/useReplaceColorParameters.ts b/frontend/editor/src/core/hooks/tools/replaceColor/useReplaceColorParameters.ts index 5304210ce3..9e6ccd2343 100644 --- a/frontend/editor/src/core/hooks/tools/replaceColor/useReplaceColorParameters.ts +++ b/frontend/editor/src/core/hooks/tools/replaceColor/useReplaceColorParameters.ts @@ -29,13 +29,19 @@ export const defaultParameters: ReplaceColorParameters = { export type ReplaceColorParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateReplaceColorParameters( + _params: ReplaceColorParameters, +): boolean { + // All parameters are always valid as they have defaults + return true; +} + export const useReplaceColorParameters = (): ReplaceColorParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "replace-invert-pdf", - validateFn: () => { - // All parameters are always valid as they have defaults - return true; - }, + validateFn: validateReplaceColorParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/rotate/useRotateOperation.ts b/frontend/editor/src/core/hooks/tools/rotate/useRotateOperation.ts index f8c6d32216..5f80714f95 100644 --- a/frontend/editor/src/core/hooks/tools/rotate/useRotateOperation.ts +++ b/frontend/editor/src/core/hooks/tools/rotate/useRotateOperation.ts @@ -10,6 +10,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateRotateParameters, RotateParameters, defaultParameters, normalizeAngle, @@ -44,6 +45,7 @@ export const buildRotateFormData = ( // Static configuration object export const rotateOperationConfig = defineSingleFileTool({ + validateParams: validateRotateParameters, buildFormData: buildRotateFormData, toApiParams: rotateToApiParams, fromApiParams: rotateFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/rotate/useRotateParameters.ts b/frontend/editor/src/core/hooks/tools/rotate/useRotateParameters.ts index 870fb10ecc..66e5670dda 100644 --- a/frontend/editor/src/core/hooks/tools/rotate/useRotateParameters.ts +++ b/frontend/editor/src/core/hooks/tools/rotate/useRotateParameters.ts @@ -25,14 +25,18 @@ export type RotateParametersHook = BaseParametersHook & { normalizeAngle: (angle: number) => number; }; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateRotateParameters(params: RotateParameters): boolean { + // Angle must be a multiple of 90 + return params.angle % 90 === 0; +} + export const useRotateParameters = (): RotateParametersHook => { const baseHook = useBaseParameters({ defaultParameters, endpointName: "rotate-pdf", - validateFn: (params) => { - // Angle must be a multiple of 90 - return params.angle % 90 === 0; - }, + validateFn: validateRotateParameters, }); // Rotate clockwise by 90 degrees diff --git a/frontend/editor/src/core/hooks/tools/sanitize/useSanitizeOperation.ts b/frontend/editor/src/core/hooks/tools/sanitize/useSanitizeOperation.ts index 26a5ca7250..d85af584b3 100644 --- a/frontend/editor/src/core/hooks/tools/sanitize/useSanitizeOperation.ts +++ b/frontend/editor/src/core/hooks/tools/sanitize/useSanitizeOperation.ts @@ -10,6 +10,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateSanitizeParameters, SanitizeParameters, defaultParameters, } from "@app/hooks/tools/sanitize/useSanitizeParameters"; @@ -56,6 +57,7 @@ export const buildSanitizeFormData = ( // Static configuration object export const sanitizeOperationConfig = defineSingleFileTool({ + validateParams: validateSanitizeParameters, buildFormData: buildSanitizeFormData, toApiParams: sanitizeToApiParams, fromApiParams: sanitizeFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/sanitize/useSanitizeParameters.ts b/frontend/editor/src/core/hooks/tools/sanitize/useSanitizeParameters.ts index f2e7ec2193..716bc90d56 100644 --- a/frontend/editor/src/core/hooks/tools/sanitize/useSanitizeParameters.ts +++ b/frontend/editor/src/core/hooks/tools/sanitize/useSanitizeParameters.ts @@ -24,12 +24,18 @@ export const defaultParameters: SanitizeParameters = { export type SanitizeParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateSanitizeParameters( + params: SanitizeParameters, +): boolean { + return Object.values(params).some((value) => value === true); +} + export const useSanitizeParameters = (): SanitizeParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "sanitize-pdf", - validateFn: (params) => { - return Object.values(params).some((value) => value === true); - }, + validateFn: validateSanitizeParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/scannerImageSplit/useScannerImageSplitOperation.ts b/frontend/editor/src/core/hooks/tools/scannerImageSplit/useScannerImageSplitOperation.ts index 26db1414d0..3a4a2e72cb 100644 --- a/frontend/editor/src/core/hooks/tools/scannerImageSplit/useScannerImageSplitOperation.ts +++ b/frontend/editor/src/core/hooks/tools/scannerImageSplit/useScannerImageSplitOperation.ts @@ -12,6 +12,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateScannerImageSplitParameters, ScannerImageSplitParameters, defaultParameters, } from "@app/hooks/tools/scannerImageSplit/useScannerImageSplitParameters"; @@ -55,6 +56,7 @@ export const buildScannerImageSplitFormData = ( // Static configuration object export const scannerImageSplitOperationConfig = defineSingleFileTool({ + validateParams: validateScannerImageSplitParameters, buildFormData: buildScannerImageSplitFormData, toApiParams: scannerImageSplitToApiParams, fromApiParams: scannerImageSplitFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/scannerImageSplit/useScannerImageSplitParameters.ts b/frontend/editor/src/core/hooks/tools/scannerImageSplit/useScannerImageSplitParameters.ts index 5a40c3034e..cf2abc4fbf 100644 --- a/frontend/editor/src/core/hooks/tools/scannerImageSplit/useScannerImageSplitParameters.ts +++ b/frontend/editor/src/core/hooks/tools/scannerImageSplit/useScannerImageSplitParameters.ts @@ -23,14 +23,20 @@ export const defaultParameters: ScannerImageSplitParameters = { export type ScannerImageSplitParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateScannerImageSplitParameters( + _params: ScannerImageSplitParameters, +): boolean { + // All parameters are numeric with defaults, validation handled by form + return true; +} + export const useScannerImageSplitParameters = (): ScannerImageSplitParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "extract-image-scans", - validateFn: () => { - // All parameters are numeric with defaults, validation handled by form - return true; - }, + validateFn: validateScannerImageSplitParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/shared/toolAutomation.ts b/frontend/editor/src/core/hooks/tools/shared/toolAutomation.ts index d567a1dad1..d3874bc840 100644 --- a/frontend/editor/src/core/hooks/tools/shared/toolAutomation.ts +++ b/frontend/editor/src/core/hooks/tools/shared/toolAutomation.ts @@ -137,6 +137,26 @@ export function stepRequiresUpload(step: WorkingToolStep): boolean { return Object.values(step.params).some(isFileValue); } +/** + * True if a step still needs the user to make a choice before it can run - the tool declares some + * of its parameters mandatory and this step has not filled them in yet. + * + * This asks the tool the same question its own Run button asks (`validateParams` is the predicate + * the tool passes `useBaseParameters` as `validateFn`), so a step is "configured" in a pipeline + * exactly when it would be runnable in the editor. Tools that declare no predicate run happily on + * their defaults, and an unknown step is nobody's to judge. + */ +export function stepNeedsConfiguring( + step: WorkingToolStep, + registry: Partial, +): boolean { + if (step.toolId === null) return false; + const config = registry[step.toolId]?.operationConfig; + if (!config?.validateParams) return false; + const merged = { ...(config.defaultParameters ?? {}), ...step.params }; + return !config.validateParams(merged); +} + /** * The tools that can be run as a backend operation step, sorted by name. Includes only automatable * tools whose endpoint resolves from defaults (so they can become a backend step); this drops diff --git a/frontend/editor/src/core/hooks/tools/shared/toolOperationTypes.ts b/frontend/editor/src/core/hooks/tools/shared/toolOperationTypes.ts index 321443f463..d4d4ccceec 100644 --- a/frontend/editor/src/core/hooks/tools/shared/toolOperationTypes.ts +++ b/frontend/editor/src/core/hooks/tools/shared/toolOperationTypes.ts @@ -79,6 +79,14 @@ interface BaseToolOperationConfig { /** Default parameter values for automation */ defaultParameters?: TParams; + /** + * Whether these parameters are complete enough to run. The same predicate a tool gives + * `useBaseParameters` as its `validateFn`, so the Run button in the editor and anything composing + * the tool without rendering it (a pipeline step, an AI-authored plan) agree on what "configured" + * means. Absent means the tool runs happily on its defaults. + */ + validateParams?: (params: TParams) => boolean; + /** * Typed frontend params -> backend request model. When a tool provides this, * it is the spec-checked source of truth for the request body and its diff --git a/frontend/editor/src/core/hooks/tools/shared/validateParamsCoverage.test.ts b/frontend/editor/src/core/hooks/tools/shared/validateParamsCoverage.test.ts new file mode 100644 index 0000000000..052d074190 --- /dev/null +++ b/frontend/editor/src/core/hooks/tools/shared/validateParamsCoverage.test.ts @@ -0,0 +1,87 @@ +import fs from "fs"; +import path from "path"; +import { describe, expect, test } from "vitest"; + +/** + * A tool declares its mandatory parameters once, as the `validateFn` it hands `useBaseParameters` - + * that is what greys out its Run button until the user has chosen. Anything composing the tool + * without rendering it (a pipeline step, an AI-authored plan) has no hook to ask, so the same + * predicate has to reach the tool's `operationConfig` as `validateParams`. + * + * Nothing in the type system ties the two together: a tool that gains a `validateFn` and forgets + * `validateParams` still compiles, and a pipeline step for it would silently look configured while + * the run failed at the backend. So this scans for the drift instead. + */ +const SRC = path.resolve(__dirname, "../../../.."); +const TOOL_ROOTS = ["core/hooks/tools", "core/components/tools"]; + +interface ToolModules { + tool: string; + paramsSource: string; + operationSource: string | null; +} + +const isModule = (file: string, suffix: string) => + file.endsWith(suffix) && !file.endsWith(".test.ts"); + +/** Source lines, minus whole-line comments - a mention of a field is not a declaration of one. */ +const code = (source: string) => + source + .split("\n") + .filter((line) => !/^\s*(\/\/|\*|\/\*)/.test(line)) + .join("\n"); + +/** + * True if the tool constrains its parameters at all. `validateFn: () => true` is not a constraint - + * it says "these defaults are always runnable", which is exactly what declaring nothing says. + */ +const hasMandatoryParams = (source: string) => + /\bvalidateFn:/.test(source) && + !/\bvalidateFn:\s*\(\)\s*=>\s*true\b/.test(source); + +function toolModules(): ToolModules[] { + const found: ToolModules[] = []; + for (const root of TOOL_ROOTS) { + const rootDir = path.join(SRC, root); + for (const tool of fs.readdirSync(rootDir)) { + const dir = path.join(rootDir, tool); + if (!fs.statSync(dir).isDirectory()) continue; + const files = fs.readdirSync(dir); + const params = files.find((f) => isModule(f, "Parameters.ts")); + if (!params) continue; + const operation = files.find((f) => isModule(f, "Operation.ts")); + found.push({ + tool, + paramsSource: code(fs.readFileSync(path.join(dir, params), "utf8")), + operationSource: operation + ? code(fs.readFileSync(path.join(dir, operation), "utf8")) + : null, + }); + } + } + return found; +} + +describe("validateParams coverage", () => { + const modules = toolModules(); + + test("the scan finds the tools at all", () => { + // Guards against the sweep passing vacuously because it looked in the wrong place. + expect(modules.length).toBeGreaterThan(20); + }); + + test("every tool with mandatory parameters exposes them to composers", () => { + const missing = modules + .filter(({ paramsSource }) => hasMandatoryParams(paramsSource)) + .filter( + ({ operationSource }) => + // A tool with no operationConfig is not composable at all, so it has nothing to tell. + operationSource !== null && + /OperationConfig\s*[:=]/.test(operationSource) && + !/\bvalidateParams:/.test(operationSource), + ) + .map(({ tool }) => tool); + + expect(missing).toEqual([]); + }); +}); diff --git a/frontend/editor/src/core/hooks/tools/sign/useSignOperation.ts b/frontend/editor/src/core/hooks/tools/sign/useSignOperation.ts index d54be4ecbf..439832e5f8 100644 --- a/frontend/editor/src/core/hooks/tools/sign/useSignOperation.ts +++ b/frontend/editor/src/core/hooks/tools/sign/useSignOperation.ts @@ -5,6 +5,7 @@ import { defineSingleFileTool, } from "@app/hooks/tools/shared/useToolOperation"; import { + validateSignParameters, SignParameters, DEFAULT_PARAMETERS, } from "@app/hooks/tools/sign/useSignParameters"; @@ -51,6 +52,7 @@ export const buildSignFormData = ( // Static configuration object export const signOperationConfig = defineSingleFileTool({ + validateParams: validateSignParameters, buildFormData: buildSignFormData, operationType: "sign", // Signing is applied client-side in the viewer (see createStampTool -> diff --git a/frontend/editor/src/core/hooks/tools/sign/useSignParameters.ts b/frontend/editor/src/core/hooks/tools/sign/useSignParameters.ts index d06c18e1e3..b981638806 100644 --- a/frontend/editor/src/core/hooks/tools/sign/useSignParameters.ts +++ b/frontend/editor/src/core/hooks/tools/sign/useSignParameters.ts @@ -32,7 +32,7 @@ export const DEFAULT_PARAMETERS: SignParameters = { textAlign: "left", }; -const validateSignParameters = (parameters: SignParameters): boolean => { +export const validateSignParameters = (parameters: SignParameters): boolean => { // Basic validation if (!parameters.signatureType) return false; diff --git a/frontend/editor/src/core/hooks/tools/split/useSplitOperation.ts b/frontend/editor/src/core/hooks/tools/split/useSplitOperation.ts index af14ca2045..832bf51a3b 100644 --- a/frontend/editor/src/core/hooks/tools/split/useSplitOperation.ts +++ b/frontend/editor/src/core/hooks/tools/split/useSplitOperation.ts @@ -12,6 +12,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateSplitParameters, SplitParameters, defaultParameters, } from "@app/hooks/tools/split/useSplitParameters"; @@ -172,6 +173,7 @@ export const getSplitEndpoint = (parameters: SplitParameters): SplitEndpoint => // Static configuration object export const splitOperationConfig = defineSingleFileTool({ + validateParams: validateSplitParameters, buildFormData: buildSplitFormData, toApiParams: splitToApiParams, fromApiParams: splitFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/split/useSplitParameters.ts b/frontend/editor/src/core/hooks/tools/split/useSplitParameters.ts index b5e2dc3212..0a2874c947 100644 --- a/frontend/editor/src/core/hooks/tools/split/useSplitParameters.ts +++ b/frontend/editor/src/core/hooks/tools/split/useSplitParameters.ts @@ -49,6 +49,35 @@ export const defaultParameters: SplitParameters = { rightToLeft: false, }; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateSplitParameters(params: SplitParameters): boolean { + switch (params.method) { + case SPLIT_METHODS.BY_PAGES: + return params.pages.trim() !== ""; + case SPLIT_METHODS.BY_SECTIONS: + if (params.hDiv === "" || params.vDiv === "") return false; + if (params.splitMode === "CUSTOM") { + return (params.customPages || "").trim() !== ""; + } + return true; + case SPLIT_METHODS.BY_SIZE: + case SPLIT_METHODS.BY_PAGE_COUNT: + case SPLIT_METHODS.BY_DOC_COUNT: + return params.splitValue.trim() !== ""; + case SPLIT_METHODS.BY_CHAPTERS: + return params.bookmarkLevel !== ""; + case SPLIT_METHODS.BY_PAGE_DIVIDER: + return true; // No required parameters + case SPLIT_METHODS.BY_POSTER: + return ( + params.pageSize !== "" && params.xFactor !== "" && params.yFactor !== "" + ); + default: + return false; + } +} + export const useSplitParameters = (): SplitParametersHook => { return useBaseParameters({ defaultParameters, @@ -56,33 +85,6 @@ export const useSplitParameters = (): SplitParametersHook => { params.method ? ENDPOINTS[params.method] : ENDPOINTS[SPLIT_METHODS.BY_PAGES], - validateFn: (params) => { - switch (params.method) { - case SPLIT_METHODS.BY_PAGES: - return params.pages.trim() !== ""; - case SPLIT_METHODS.BY_SECTIONS: - if (params.hDiv === "" || params.vDiv === "") return false; - if (params.splitMode === "CUSTOM") { - return (params.customPages || "").trim() !== ""; - } - return true; - case SPLIT_METHODS.BY_SIZE: - case SPLIT_METHODS.BY_PAGE_COUNT: - case SPLIT_METHODS.BY_DOC_COUNT: - return params.splitValue.trim() !== ""; - case SPLIT_METHODS.BY_CHAPTERS: - return params.bookmarkLevel !== ""; - case SPLIT_METHODS.BY_PAGE_DIVIDER: - return true; // No required parameters - case SPLIT_METHODS.BY_POSTER: - return ( - params.pageSize !== "" && - params.xFactor !== "" && - params.yFactor !== "" - ); - default: - return false; - } - }, + validateFn: validateSplitParameters, }); }; diff --git a/frontend/editor/src/core/hooks/tools/timestampPdf/useTimestampPdfOperation.ts b/frontend/editor/src/core/hooks/tools/timestampPdf/useTimestampPdfOperation.ts index 0769f37952..2faf90c660 100644 --- a/frontend/editor/src/core/hooks/tools/timestampPdf/useTimestampPdfOperation.ts +++ b/frontend/editor/src/core/hooks/tools/timestampPdf/useTimestampPdfOperation.ts @@ -10,6 +10,7 @@ import { } from "@app/hooks/tools/shared/toolApiMapping"; import { createStandardErrorHandler } from "@app/utils/toolErrorHandler"; import { + validateTimestampPdfParameters, TimestampPdfParameters, defaultParameters, } from "@app/hooks/tools/timestampPdf/useTimestampPdfParameters"; @@ -36,6 +37,7 @@ export const buildTimestampPdfFormData = ( objectToFormData(timestampPdfToApiParams(parameters), { fileInput: file }); export const timestampPdfOperationConfig = defineSingleFileTool({ + validateParams: validateTimestampPdfParameters, buildFormData: buildTimestampPdfFormData, toApiParams: timestampPdfToApiParams, fromApiParams: timestampPdfFromApiParams, diff --git a/frontend/editor/src/core/hooks/tools/timestampPdf/useTimestampPdfParameters.ts b/frontend/editor/src/core/hooks/tools/timestampPdf/useTimestampPdfParameters.ts index 8eebe34e7d..ab99243787 100644 --- a/frontend/editor/src/core/hooks/tools/timestampPdf/useTimestampPdfParameters.ts +++ b/frontend/editor/src/core/hooks/tools/timestampPdf/useTimestampPdfParameters.ts @@ -26,12 +26,18 @@ export const defaultParameters: TimestampPdfParameters = { export type TimestampPdfParametersHook = BaseParametersHook; +/** Whether these parameters are complete enough to run. Shared by the tool's settings + * hook and its operationConfig, so the editor and the pipeline builder agree. */ +export function validateTimestampPdfParameters( + params: TimestampPdfParameters, +): boolean { + return params.tsaUrl.trim().length > 0; +} + export const useTimestampPdfParameters = (): TimestampPdfParametersHook => { return useBaseParameters({ defaultParameters, endpointName: "timestamp-pdf", - validateFn: (params) => { - return params.tsaUrl.trim().length > 0; - }, + validateFn: validateTimestampPdfParameters, }); }; diff --git a/frontend/editor/src/portal/views/PipelineBuilder.tsx b/frontend/editor/src/portal/views/PipelineBuilder.tsx index c7a873fe41..4227dbcd11 100644 --- a/frontend/editor/src/portal/views/PipelineBuilder.tsx +++ b/frontend/editor/src/portal/views/PipelineBuilder.tsx @@ -26,6 +26,7 @@ import { getExecutableTools, newWorkingToolStep, serializeToolStep, + stepNeedsConfiguring, stepRequiresUpload, type ExecutableTool, type WorkingToolStep, @@ -389,10 +390,15 @@ export function PipelineBuilder() { const uploadStepLabels = steps.filter(stepRequiresUpload).map(stepLabel); const hasUploadSteps = uploadStepLabels.length > 0; - // An integration step with no operation or no account chosen would fail at run time with a raw - // backend rejection, so block saving on it here where the fix is one click away. + // A step still missing a choice - an integration with no operation or account, a tool whose + // mandatory parameters are unset - would fail at run time with a raw backend rejection, so block + // saving on it here where the fix is one click away. const unconfiguredStepLabels = steps - .filter((step) => !integrationStepConfigured(step)) + .filter( + (step) => + !integrationStepConfigured(step) || + stepNeedsConfiguring(step, allTools), + ) .map(stepLabel); const hasUnconfiguredSteps = unconfiguredStepLabels.length > 0; @@ -867,6 +873,10 @@ export function PipelineBuilder() { {t("portal.pipelines.builder.needsUpload")} + ) : stepNeedsConfiguring(step, allTools) ? ( + + {t("portal.pipelines.builder.needsConfiguring")} + ) : step.support === "unsupported" ? ( {t("portal.pipelines.builder.usesDefaults")}