mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
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. <img width="1264" height="503" alt="image" src="https://github.com/user-attachments/assets/f320579c-6c08-4c66-a999-015d46df1b33" />
This commit is contained in:
@@ -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?"
|
||||
|
||||
@@ -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,
|
||||
|
||||
+7
-3
@@ -30,12 +30,16 @@ export const defaultParameters: AddPageNumbersParameters = {
|
||||
export type AddPageNumbersParametersHook =
|
||||
BaseParametersHook<AddPageNumbersParameters>;
|
||||
|
||||
export function validateAddPageNumbersParameters(
|
||||
params: AddPageNumbersParameters,
|
||||
): boolean {
|
||||
return params.fontSize > 0 && params.startingNumber > 0;
|
||||
}
|
||||
|
||||
export const useAddPageNumbersParameters = (): AddPageNumbersParametersHook => {
|
||||
return useBaseParameters<AddPageNumbersParameters>({
|
||||
defaultParameters,
|
||||
endpointName: "add-page-numbers",
|
||||
validateFn: (params): boolean => {
|
||||
return params.fontSize > 0 && params.startingNumber > 0;
|
||||
},
|
||||
validateFn: validateAddPageNumbersParameters,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -39,16 +39,20 @@ export const defaultParameters: AddStampParameters = {
|
||||
|
||||
export type AddStampParametersHook = BaseParametersHook<AddStampParameters>;
|
||||
|
||||
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<AddStampParameters>({
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -34,19 +34,25 @@ export const defaultParameters: AddWatermarkParameters = {
|
||||
export type AddWatermarkParametersHook =
|
||||
BaseParametersHook<AddWatermarkParameters>;
|
||||
|
||||
/** 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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
+9
-3
@@ -34,13 +34,19 @@ export const defaultParameters: AdjustPageScaleParameters = {
|
||||
export type AdjustPageScaleParametersHook =
|
||||
BaseParametersHook<AdjustPageScaleParameters>;
|
||||
|
||||
/** 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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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<AutoRotateParameters>(
|
||||
toApiParams: autoRotateToApiParams,
|
||||
fromApiParams: autoRotateFromApiParams,
|
||||
defaultParameters,
|
||||
validateParams: validateAutoRotateParameters,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -23,9 +23,15 @@ export const defaultParameters: AutoRotateParameters = {
|
||||
|
||||
export type AutoRotateParametersHook = BaseParametersHook<AutoRotateParameters>;
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+9
-3
@@ -29,13 +29,19 @@ export const defaultParameters: BookletImpositionParameters = {
|
||||
export type BookletImpositionParametersHook =
|
||||
BaseParametersHook<BookletImpositionParameters>;
|
||||
|
||||
/** 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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -46,39 +46,45 @@ export const defaultParameters: CertSignParameters = {
|
||||
|
||||
export type CertSignParametersHook = BaseParametersHook<CertSignParameters>;
|
||||
|
||||
/** 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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+11
@@ -29,10 +29,21 @@ export const defaultParameters: ChangePermissionsParameters = {
|
||||
export type ChangePermissionsParametersHook =
|
||||
BaseParametersHook<ChangePermissionsParameters>;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -26,15 +26,20 @@ export const defaultParameters: CompareParameters = {
|
||||
|
||||
export type CompareParametersHook = BaseParametersHook<CompareParameters>;
|
||||
|
||||
/** 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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -32,20 +32,26 @@ export const defaultParameters: CompressParameters = {
|
||||
|
||||
export type CompressParametersHook = BaseParametersHook<CompressParameters>;
|
||||
|
||||
/** 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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -42,15 +42,19 @@ export type CropParametersHook = BaseParametersHook<CropParameters> & {
|
||||
) => 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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -15,10 +15,18 @@ export const defaultParameters: ExtractPagesParameters = {
|
||||
export type ExtractPagesParametersHook =
|
||||
BaseParametersHook<ExtractPagesParameters>;
|
||||
|
||||
/** 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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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<OverlayPdfsParameters> =
|
||||
defineSingleFileTool({
|
||||
validateParams: validateOverlayPdfsParameters,
|
||||
buildFormData,
|
||||
toApiParams: overlayPdfsToApiParams,
|
||||
fromApiParams: overlayPdfsFromApiParams,
|
||||
|
||||
@@ -27,24 +27,25 @@ export const defaultParameters: OverlayPdfsParameters = {
|
||||
export type OverlayPdfsParametersHook =
|
||||
BaseParametersHook<OverlayPdfsParameters>;
|
||||
|
||||
/** 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<OverlayPdfsParameters>({
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -40,41 +40,47 @@ export const defaultParameters: PageLayoutParameters = {
|
||||
|
||||
export type PageLayoutParametersHook = BaseParametersHook<PageLayoutParameters>;
|
||||
|
||||
/** 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<PageLayoutParameters>({
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -30,6 +30,19 @@ export const defaultParameters: RedactParameters = {
|
||||
|
||||
export type RedactParametersHook = BaseParametersHook<RedactParameters>;
|
||||
|
||||
/** 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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -19,14 +19,23 @@ export const defaultParameters: RemoveBlanksParameters = {
|
||||
export type RemoveBlanksParametersHook =
|
||||
BaseParametersHook<RemoveBlanksParameters>;
|
||||
|
||||
/** 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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -16,10 +16,18 @@ export const defaultParameters: RemovePagesParameters = {
|
||||
export type RemovePagesParametersHook =
|
||||
BaseParametersHook<RemovePagesParameters>;
|
||||
|
||||
/** 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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -29,13 +29,19 @@ export const defaultParameters: ReplaceColorParameters = {
|
||||
export type ReplaceColorParametersHook =
|
||||
BaseParametersHook<ReplaceColorParameters>;
|
||||
|
||||
/** 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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -25,14 +25,18 @@ export type RotateParametersHook = BaseParametersHook<RotateParameters> & {
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -24,12 +24,18 @@ export const defaultParameters: SanitizeParameters = {
|
||||
|
||||
export type SanitizeParametersHook = BaseParametersHook<SanitizeParameters>;
|
||||
|
||||
/** 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,
|
||||
});
|
||||
};
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+10
-4
@@ -23,14 +23,20 @@ export const defaultParameters: ScannerImageSplitParameters = {
|
||||
export type ScannerImageSplitParametersHook =
|
||||
BaseParametersHook<ScannerImageSplitParameters>;
|
||||
|
||||
/** 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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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<ToolRegistry>,
|
||||
): 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
|
||||
|
||||
@@ -79,6 +79,14 @@ interface BaseToolOperationConfig<TParams, TEndpoint extends ToolEndpoint> {
|
||||
/** 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
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -26,12 +26,18 @@ export const defaultParameters: TimestampPdfParameters = {
|
||||
export type TimestampPdfParametersHook =
|
||||
BaseParametersHook<TimestampPdfParameters>;
|
||||
|
||||
/** 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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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() {
|
||||
<span className="portal-builder__step-note">
|
||||
{t("portal.pipelines.builder.needsUpload")}
|
||||
</span>
|
||||
) : stepNeedsConfiguring(step, allTools) ? (
|
||||
<span className="portal-builder__step-note">
|
||||
{t("portal.pipelines.builder.needsConfiguring")}
|
||||
</span>
|
||||
) : step.support === "unsupported" ? (
|
||||
<span className="portal-builder__step-note">
|
||||
{t("portal.pipelines.builder.usesDefaults")}
|
||||
|
||||
Reference in New Issue
Block a user