From 41181c9da197c670bf46b46569dd2e67ba8d242d Mon Sep 17 00:00:00 2001 From: James Brunton Date: Tue, 23 Jun 2026 16:44:52 +0100 Subject: [PATCH] Redesign tool config types to avoid `any` typing (#6582) # Description of Changes Fixes one of the main causes of `any` typing left in tools, the way that we register tool parameters in the registry. Currently, it just accepts tool params via `any`, but instead we can explicitly change them to `Record void; + onSave: (parameters: ErasedToolParams) => void; onCancel: () => void; toolRegistry: Partial; } @@ -40,7 +40,7 @@ export default function ToolConfigurationModal({ }: ToolConfigurationModalProps) { const { t } = useTranslation(); - const [parameters, setParameters] = useState({}); + const [parameters, setParameters] = useState({}); // Get tool info from registry const toolInfo = toolRegistry[tool.operation as ToolId]; @@ -74,26 +74,11 @@ export default function ToolConfigurationModal({ ); } - // Special handling for ConvertSettings which needs additional props - if (tool.operation === "convert") { - return ( - { - setParameters((prev: any) => ({ ...prev, [key]: value })); - }} - getAvailableToExtensions={getAvailableToExtensions} - selectedFiles={[]} - disabled={false} - /> - ); - } - return ( { - setParameters((prev: any) => ({ ...prev, [key]: value })); + onParameterChange={(key, value) => { + setParameters((prev) => ({ ...prev, [key]: value })); }} disabled={false} /> diff --git a/frontend/editor/src/core/components/tools/automate/ToolList.tsx b/frontend/editor/src/core/components/tools/automate/ToolList.tsx index 7345f0a9b0..e57e96daa3 100644 --- a/frontend/editor/src/core/components/tools/automate/ToolList.tsx +++ b/frontend/editor/src/core/components/tools/automate/ToolList.tsx @@ -18,7 +18,7 @@ interface ToolListProps { onToolConfigure: (index: number) => void; onToolAdd: () => void; getToolName: (operation: string) => string; - getToolDefaultParameters: (operation: string) => Record; + getToolDefaultParameters: (operation: string) => Record; } export default function ToolList({ diff --git a/frontend/editor/src/core/components/tools/convert/ConvertSettings.tsx b/frontend/editor/src/core/components/tools/convert/ConvertSettings.tsx index 8b72e20ad5..5c0843660f 100644 --- a/frontend/editor/src/core/components/tools/convert/ConvertSettings.tsx +++ b/frontend/editor/src/core/components/tools/convert/ConvertSettings.tsx @@ -10,7 +10,11 @@ import { import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import { useTranslation } from "react-i18next"; import { useMultipleEndpointsEnabled } from "@app/hooks/useEndpointConfig"; -import { isImageFormat, isWebFormat } from "@app/utils/convertUtils"; +import { + isImageFormat, + isWebFormat, + getAvailableToExtensions as defaultGetAvailableToExtensions, +} from "@app/utils/convertUtils"; import { getConversionEndpoints } from "@app/data/toolsTaxonomy"; import { useFileSelection } from "@app/contexts/FileContext"; import { useFileState } from "@app/contexts/FileContext"; @@ -47,18 +51,18 @@ interface ConvertSettingsProps { key: K, value: ConvertParameters[K], ) => void; - getAvailableToExtensions: ( + getAvailableToExtensions?: ( fromExtension: string, ) => Array<{ value: string; label: string; group: string }>; - selectedFiles: StirlingFile[]; + selectedFiles?: StirlingFile[]; disabled?: boolean; } const ConvertSettings = ({ parameters, onParameterChange, - getAvailableToExtensions, - selectedFiles, + getAvailableToExtensions = defaultGetAvailableToExtensions, + selectedFiles = [], disabled = false, }: ConvertSettingsProps) => { const { t } = useTranslation(); diff --git a/frontend/editor/src/core/components/tools/shared/FilesToolStep.tsx b/frontend/editor/src/core/components/tools/shared/FilesToolStep.tsx index bca4f6aaa8..19701c2755 100644 --- a/frontend/editor/src/core/components/tools/shared/FilesToolStep.tsx +++ b/frontend/editor/src/core/components/tools/shared/FilesToolStep.tsx @@ -10,14 +10,20 @@ export interface FilesToolStepProps { minFiles?: number; } -export function createFilesToolStep( +interface StepBaseProps { + isVisible?: boolean; + isCollapsed?: boolean; + onCollapsedClick?: () => void; +} + +export function createFilesToolStep( createStep: ( title: string, - props: any, + props: StepBaseProps, children?: React.ReactNode, - ) => React.ReactElement, + ) => T, props: FilesToolStepProps, -): React.ReactElement { +): T { return createStep( i18n.t("files.title", "Files"), { diff --git a/frontend/editor/src/core/data/toolsTaxonomy.ts b/frontend/editor/src/core/data/toolsTaxonomy.ts index b12ab38813..18b8561535 100644 --- a/frontend/editor/src/core/data/toolsTaxonomy.ts +++ b/frontend/editor/src/core/data/toolsTaxonomy.ts @@ -1,6 +1,10 @@ import { type TFunction } from "i18next"; import React from "react"; -import { ToolOperationConfig } from "@app/hooks/tools/shared/toolOperationTypes"; +import { + type ErasedToolParams, + type ToolAutomationSettingsProps, + ToolOperationConfig, +} from "@app/hooks/tools/shared/toolOperationTypes"; import { BaseToolProps } from "@app/types/tool"; import { WorkbenchType } from "@app/types/workbench"; import { @@ -58,10 +62,13 @@ export type ToolRegistryEntry = { kind?: ToolKind; // Workbench type for navigation workbench?: WorkbenchType; - // Operation configuration for automation - operationConfig?: ToolOperationConfig; - // Settings component for automation configuration - automationSettings: React.ComponentType | null; + // Operation configuration for automation. TParams is erased at the registry + // boundary; tools are authored type-safely via defineToolAutomation. + operationConfig?: ToolOperationConfig; + // Settings component for automation configuration. + automationSettings: React.ComponentType< + ToolAutomationSettingsProps + > | null; // Whether this tool supports automation (defaults to true) supportsAutomate?: boolean; // Synonyms for search (optional) diff --git a/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx b/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx index 74b676dceb..605c6a3925 100644 --- a/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx +++ b/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx @@ -12,6 +12,10 @@ import { LinkToolRegistry, } from "@app/data/toolsTaxonomy"; import { isSuperToolId, isLinkToolId } from "@app/types/toolId"; +import { + asRegistryConfig, + lazySettings, +} from "@app/hooks/tools/shared/toolOperationTypes"; import { adjustContrastOperationConfig } from "@app/hooks/tools/adjustContrast/useAdjustContrastOperation"; import { getSynonyms } from "@app/utils/toolSynonyms"; import { useProprietaryToolRegistry } from "@app/data/useProprietaryToolRegistry"; @@ -143,8 +147,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, endpoints: ["merge-pdfs"], - operationConfig: mergeOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(mergeOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/merge/MergeSettings"), ), synonyms: getSynonyms(t, "merge"), @@ -169,8 +173,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { synonyms: getSynonyms(t, "certSign"), maxFiles: -1, endpoints: ["cert-sign"], - operationConfig: certSignOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(certSignOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/certSign/CertSignAutomationSettings"), ), @@ -193,7 +197,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.SIGNING, maxFiles: -1, endpoints: ["timestamp-pdf"], - operationConfig: timestampPdfOperationConfig, + operationConfig: asRegistryConfig(timestampPdfOperationConfig), automationSettings: null, synonyms: getSynonyms(t, "timestampPdf"), }, @@ -210,8 +214,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.SIGNING, endpoints: ["sign"], - operationConfig: signOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(signOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/sign/SignSettings"), ), // TODO:: not all settings shown, suggested next tools shown synonyms: getSynonyms(t, "sign"), @@ -234,7 +238,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.GENERAL, endpoints: ["sign"], - operationConfig: signOperationConfig, + operationConfig: asRegistryConfig(signOperationConfig), automationSettings: null, synonyms: getSynonyms(t, "addText"), supportsAutomate: false, @@ -253,7 +257,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.GENERAL, endpoints: ["add-image"], - operationConfig: signOperationConfig, + operationConfig: asRegistryConfig(signOperationConfig), automationSettings: null, synonyms: getSynonyms(t, "addImage"), supportsAutomate: false, @@ -276,7 +280,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.GENERAL, workbench: "viewer", endpoints: ["view-pdf"], - operationConfig: signOperationConfig, + operationConfig: asRegistryConfig(signOperationConfig), automationSettings: null, synonyms: getSynonyms(t, "annotate"), supportsAutomate: false, @@ -298,8 +302,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["add-password"], - operationConfig: addPasswordOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(addPasswordOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/addPassword/AddPasswordSettings"), ), synonyms: getSynonyms(t, "addPassword"), @@ -322,8 +326,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.DOCUMENT_SECURITY, endpoints: ["add-watermark"], - operationConfig: addWatermarkOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(addWatermarkOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/addWatermark/AddWatermarkSingleStepSettings"), ), @@ -348,8 +352,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { synonyms: getSynonyms(t, "addStamp"), maxFiles: -1, endpoints: ["add-stamp"], - operationConfig: addStampOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(addStampOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/addStamp/AddStampAutomationSettings"), ), @@ -372,8 +376,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { "Remove potentially harmful elements from PDF files", ), endpoints: ["sanitize-pdf"], - operationConfig: sanitizeOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(sanitizeOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/sanitize/SanitizeSettings"), ), synonyms: getSynonyms(t, "sanitize"), @@ -396,8 +400,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["flatten"], - operationConfig: flattenOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(flattenOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/flatten/FlattenSettings"), ), synonyms: getSynonyms(t, "flatten"), @@ -420,7 +424,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["unlock-pdf-forms"], - operationConfig: unlockPdfFormsOperationConfig, + operationConfig: asRegistryConfig(unlockPdfFormsOperationConfig), synonyms: getSynonyms(t, "unlockPDFForms"), automationSettings: null, }, @@ -458,8 +462,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["add-password"], - operationConfig: changePermissionsOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(changePermissionsOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/changePermissions/ChangePermissionsSettings"), ), @@ -551,8 +555,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.DOCUMENT_REVIEW, maxFiles: -1, endpoints: ["update-metadata"], - operationConfig: changeMetadataOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(changeMetadataOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/changeMetadata/ChangeMetadataSingleStep"), ), @@ -570,7 +574,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.DOCUMENT_REVIEW, maxFiles: 1, endpoints: ["edit-table-of-contents"], - operationConfig: editTableOfContentsOperationConfig, + operationConfig: asRegistryConfig(editTableOfContentsOperationConfig), automationSettings: null, supportsAutomate: false, synonyms: getSynonyms(t, "editTableOfContents"), @@ -589,8 +593,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["crop"], - operationConfig: cropOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(cropOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/crop/CropAutomationSettings"), ), }, @@ -609,8 +613,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["rotate-pdf"], - operationConfig: rotateOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(rotateOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/rotate/RotateAutomationSettings"), ), synonyms: getSynonyms(t, "rotate"), @@ -629,8 +633,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, endpoints: Array.from(new Set(Object.values(SPLIT_ENDPOINT_NAMES))), - operationConfig: splitOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(splitOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/split/SplitAutomationSettings"), ), synonyms: getSynonyms(t, "split"), @@ -648,7 +652,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, endpoints: ["rearrange-pages"], - operationConfig: reorganizePagesOperationConfig, + operationConfig: asRegistryConfig(reorganizePagesOperationConfig), synonyms: getSynonyms(t, "reorganizePages"), automationSettings: null, }, @@ -666,8 +670,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["scale-pages"], - operationConfig: adjustPageScaleOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(adjustPageScaleOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/adjustPageScale/AdjustPageScaleSettings"), ), @@ -683,13 +687,13 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { ), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, - automationSettings: lazy( + automationSettings: lazySettings( () => import("@app/components/tools/addPageNumbers/AddPageNumbersAutomationSettings"), ), maxFiles: -1, endpoints: ["add-page-numbers"], - operationConfig: addPageNumbersOperationConfig, + operationConfig: asRegistryConfig(addPageNumbersOperationConfig), synonyms: getSynonyms(t, "addPageNumbers"), }, pageLayout: { @@ -710,7 +714,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["multi-page-layout"], - automationSettings: lazy( + automationSettings: lazySettings( () => import("@app/components/tools/pageLayout/PageLayoutSettings"), ), synonyms: getSynonyms(t, "pageLayout"), @@ -725,8 +729,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { ), name: t("home.bookletImposition.title", "Booklet Imposition"), component: lazy(() => import("@app/tools/BookletImposition")), - operationConfig: bookletImpositionOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(bookletImpositionOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/bookletImposition/BookletImpositionSettings"), ), @@ -757,7 +761,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["pdf-to-single-page"], - operationConfig: singleLargePageOperationConfig, + operationConfig: asRegistryConfig(singleLargePageOperationConfig), synonyms: getSynonyms(t, "pdfToSinglePage"), automationSettings: null, }, @@ -776,8 +780,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { synonyms: getSynonyms(t, "addAttachments"), maxFiles: 1, endpoints: ["add-attachments"], - operationConfig: addAttachmentsOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(addAttachmentsOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/addAttachments/AddAttachmentsSettings"), ), @@ -798,11 +802,11 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.EXTRACTION, synonyms: getSynonyms(t, "extractPages"), - automationSettings: lazy( + automationSettings: lazySettings( () => import("@app/components/tools/extractPages/ExtractPagesSettings"), ), - operationConfig: extractPagesOperationConfig, + operationConfig: asRegistryConfig(extractPagesOperationConfig), endpoints: ["rearrange-pages"], }, extractImages: { @@ -823,8 +827,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.EXTRACTION, maxFiles: -1, endpoints: ["extract-images"], - operationConfig: extractImagesOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(extractImagesOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/extractImages/ExtractImagesSettings"), ), @@ -852,8 +856,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: 1, endpoints: ["remove-pages"], synonyms: getSynonyms(t, "removePages"), - operationConfig: removePagesOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(removePagesOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/removePages/RemovePagesSettings"), ), }, @@ -876,8 +880,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: 1, endpoints: ["remove-blanks"], synonyms: getSynonyms(t, "removeBlanks"), - operationConfig: removeBlanksOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(removeBlanksOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/removeBlanks/RemoveBlanksSettings"), ), @@ -900,7 +904,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.REMOVAL, maxFiles: -1, endpoints: ["remove-annotations"], - operationConfig: removeAnnotationsOperationConfig, + operationConfig: asRegistryConfig(removeAnnotationsOperationConfig), automationSettings: null, synonyms: getSynonyms(t, "removeAnnotations"), }, @@ -944,8 +948,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.REMOVAL, endpoints: ["remove-password"], maxFiles: -1, - operationConfig: removePasswordOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(removePasswordOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/removePassword/RemovePasswordSettings"), ), @@ -969,7 +973,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.REMOVAL, maxFiles: -1, endpoints: ["remove-cert-sign"], - operationConfig: removeCertificateSignOperationConfig, + operationConfig: asRegistryConfig(removeCertificateSignOperationConfig), synonyms: getSynonyms(t, "removeCertSign"), automationSettings: null, }, @@ -1002,7 +1006,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { component: lazy(() => import("@app/tools/AutoRename")), maxFiles: -1, endpoints: ["auto-rename"], - operationConfig: autoRenameOperationConfig, + operationConfig: asRegistryConfig(autoRenameOperationConfig), description: t( "home.autoRename.desc", "Automatically rename PDF files based on their content", @@ -1028,8 +1032,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.ADVANCED_FORMATTING, maxFiles: -1, endpoints: ["adjust-contrast"], - operationConfig: adjustContrastOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(adjustContrastOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/adjustContrast/AdjustContrastSingleStepSettings"), ), @@ -1053,7 +1057,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.ADVANCED_FORMATTING, maxFiles: -1, endpoints: ["repair"], - operationConfig: repairOperationConfig, + operationConfig: asRegistryConfig(repairOperationConfig), synonyms: getSynonyms(t, "repair"), automationSettings: null, }, @@ -1078,8 +1082,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.ADVANCED_FORMATTING, maxFiles: -1, endpoints: ["extract-image-scans"], - operationConfig: scannerImageSplitOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(scannerImageSplitOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/scannerImageSplit/ScannerImageSplitSettings"), ), @@ -1102,9 +1106,9 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.ADVANCED_FORMATTING, endpoints: ["overlay-pdf"], - operationConfig: overlayPdfsOperationConfig, + operationConfig: asRegistryConfig(overlayPdfsOperationConfig), synonyms: getSynonyms(t, "overlay-pdfs"), - automationSettings: lazy( + automationSettings: lazySettings( () => import("@app/components/tools/overlayPdfs/OverlayPdfsSettings"), ), }, @@ -1126,8 +1130,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.ADVANCED_FORMATTING, maxFiles: -1, endpoints: ["replace-invert-pdf"], - operationConfig: replaceColorOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(replaceColorOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/replaceColor/ReplaceColorSettings"), ), @@ -1299,8 +1303,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, endpoints: ["compress-pdf"], - operationConfig: compressOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(compressOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/compress/CompressSettings"), ), synonyms: getSynonyms(t, "compress"), @@ -1338,8 +1342,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { "pdf-to-epub", ], - operationConfig: convertOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(convertOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/convert/ConvertSettings"), ), synonyms: getSynonyms(t, "convert"), @@ -1363,8 +1367,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, endpoints: ["ocr-pdf"], - operationConfig: ocrOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(ocrOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/ocr/OCRSettings"), ), synonyms: getSynonyms(t, "ocr"), @@ -1387,8 +1391,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, endpoints: ["auto-redact"], - operationConfig: redactOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(redactOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/redact/RedactSingleStepSettings"), ), synonyms: getSynonyms(t, "redact"), diff --git a/frontend/editor/src/core/hooks/tools/automate/useAutomationForm.ts b/frontend/editor/src/core/hooks/tools/automate/useAutomationForm.ts index 91f908c05a..a27cf1f7a3 100644 --- a/frontend/editor/src/core/hooks/tools/automate/useAutomationForm.ts +++ b/frontend/editor/src/core/hooks/tools/automate/useAutomationForm.ts @@ -40,7 +40,7 @@ export function useAutomationForm({ ); const getToolDefaultParameters = useCallback( - (operation: string): Record => { + (operation: string): Record => { const config = toolRegistry[operation as ToolId]?.operationConfig; if (config?.defaultParameters) { return { ...config.defaultParameters }; diff --git a/frontend/editor/src/core/hooks/tools/shared/toolOperationTypes.ts b/frontend/editor/src/core/hooks/tools/shared/toolOperationTypes.ts index 27cc16b13a..0d48ce6d09 100644 --- a/frontend/editor/src/core/hooks/tools/shared/toolOperationTypes.ts +++ b/frontend/editor/src/core/hooks/tools/shared/toolOperationTypes.ts @@ -1,3 +1,4 @@ +import { lazy, type ComponentType } from "react"; import { StirlingFile } from "@app/types/fileContext"; import type { ResponseHandler } from "@app/utils/toolResponseProcessor"; import { ToolId } from "@app/types/toolId"; @@ -147,6 +148,56 @@ export type ToolOperationConfig = | MultiFileToolOperationConfig | CustomToolOperationConfig; +/** + * One generic source-of-truth for the props every automation settings component + * accepts: the tool's parameters plus a typed change handler. + */ +export interface ToolAutomationSettingsProps { + parameters: TParams; + onParameterChange: ( + key: K, + value: TParams[K], + ) => void; + disabled?: boolean; +} + +/** + * Erased parameter shape stored in the registry. Spreadable and callable, so + * consumers can merge defaults and invoke buildFormData/customProcessor/endpoint + * without per-tool type knowledge. + */ +export type ErasedToolParams = Record; + +export type RegistryToolOperationConfig = ToolOperationConfig; +export type RegistryAutomationSettings = ComponentType< + ToolAutomationSettingsProps +> | null; + +/** + * Store a tool's typed operationConfig in the registry. The input is validated as + * a real ToolOperationConfig, then TParams is erased here. TParams is + * invariant in ToolOperationConfig, so the erasure cannot be a plain assignment; + * the `as unknown as` is the localized existential boundary. + */ +export function asRegistryConfig( + config: ToolOperationConfig, +): RegistryToolOperationConfig { + return config as unknown as RegistryToolOperationConfig; +} + +/** + * Lazily load a tool's automation settings component for the registry. The loaded + * component is validated against ToolAutomationSettingsProps (inferred + * from the module), then erased to the registry's shared props shape. + */ +export function lazySettings( + loader: () => Promise<{ + default: ComponentType>; + }>, +): RegistryAutomationSettings { + return lazy(loader) as unknown as RegistryAutomationSettings; +} + /** * Complete tool operation interface returned by useToolOperation. */ diff --git a/frontend/editor/src/core/tools/Automate.tsx b/frontend/editor/src/core/tools/Automate.tsx index 6952bb31c1..5a197c357a 100644 --- a/frontend/editor/src/core/tools/Automate.tsx +++ b/frontend/editor/src/core/tools/Automate.tsx @@ -4,7 +4,10 @@ import { useViewScopedFiles } from "@app/hooks/tools/shared/useViewScopedFiles"; import { useNavigationActions } from "@app/contexts/NavigationContext"; import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext"; -import { createToolFlow } from "@app/components/tools/shared/createToolFlow"; +import { + createToolFlow, + type MiddleStepConfig, +} from "@app/components/tools/shared/createToolFlow"; import { createFilesToolStep } from "@app/components/tools/shared/FilesToolStep"; import AutomationSelection from "@app/components/tools/automate/AutomationSelection"; import AutomationCreation from "@app/components/tools/automate/AutomationCreation"; @@ -200,7 +203,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const createStep = ( title: string, - props: any, + props: Omit, content?: React.ReactNode, ) => ({ title, diff --git a/frontend/editor/src/core/utils/automationExecutor.ts b/frontend/editor/src/core/utils/automationExecutor.ts index 139fca981a..a29ffbcb83 100644 --- a/frontend/editor/src/core/utils/automationExecutor.ts +++ b/frontend/editor/src/core/utils/automationExecutor.ts @@ -4,6 +4,11 @@ import { ToolId } from "@app/types/toolId"; import { AUTOMATION_CONSTANTS } from "@app/constants/automation"; import { AutomationFileProcessor } from "@app/utils/automationFileProcessor"; import { ToolType } from "@app/hooks/tools/shared/useToolOperation"; +import { + type ErasedToolParams, + type MultiFileToolOperationConfig, + type SingleFileToolOperationConfig, +} from "@app/hooks/tools/shared/toolOperationTypes"; import { zipFileService } from "@app/services/zipFileService"; import { processResponse } from "@app/utils/toolResponseProcessor"; @@ -76,8 +81,8 @@ const executeApiRequest = async ( * Execute single-file tool operation (processes files one at a time) */ const executeSingleFileOperation = async ( - config: any, - parameters: any, + config: SingleFileToolOperationConfig, + parameters: ErasedToolParams, files: File[], filePrefix: string, ): Promise => { @@ -89,9 +94,7 @@ const executeSingleFileOperation = async ( ? config.endpoint(parameters) : config.endpoint; - const formData = ( - config.buildFormData as (params: any, file: File) => FormData - )(parameters, file); + const formData = config.buildFormData(parameters, file); const processedFiles = await executeApiRequest( endpoint, @@ -110,8 +113,8 @@ const executeSingleFileOperation = async ( * Execute multi-file tool operation (processes all files in one request) */ const executeMultiFileOperation = async ( - config: any, - parameters: any, + config: MultiFileToolOperationConfig, + parameters: ErasedToolParams, files: File[], filePrefix: string, ): Promise => { @@ -120,9 +123,7 @@ const executeMultiFileOperation = async ( ? config.endpoint(parameters) : config.endpoint; - const formData = ( - config.buildFormData as (params: any, files: File[]) => FormData - )(parameters, files); + const formData = config.buildFormData(parameters, files); return await executeApiRequest( endpoint, @@ -138,7 +139,7 @@ const executeMultiFileOperation = async ( */ export const executeToolOperation = async ( operationName: string, - parameters: any, + parameters: ErasedToolParams, files: File[], toolRegistry: ToolRegistry, ): Promise => { @@ -156,7 +157,7 @@ export const executeToolOperation = async ( */ export const executeToolOperationWithPrefix = async ( operationName: string, - parameters: any, + parameters: ErasedToolParams, files: File[], toolRegistry: ToolRegistry, filePrefix: string = AUTOMATION_CONSTANTS.FILE_PREFIX, diff --git a/frontend/editor/src/prototypes/data/usePrototypeToolRegistry.tsx b/frontend/editor/src/prototypes/data/usePrototypeToolRegistry.tsx index 3ebcfeade5..7267bed751 100644 --- a/frontend/editor/src/prototypes/data/usePrototypeToolRegistry.tsx +++ b/frontend/editor/src/prototypes/data/usePrototypeToolRegistry.tsx @@ -8,6 +8,7 @@ import { type PrototypeToolRegistry, } from "@app/data/toolsTaxonomy"; import { pdfCommentAgentOperationConfig } from "@app/hooks/tools/pdfCommentAgent/pdfCommentAgentOperationConfig"; +import { asRegistryConfig } from "@app/hooks/tools/shared/toolOperationTypes"; import PdfCommentAgent from "@app/tools/PdfCommentAgent"; import { getSynonyms } from "@app/utils/toolSynonyms"; @@ -42,7 +43,7 @@ export function usePrototypeToolRegistry(): PrototypeToolRegistry { subcategoryId: SubcategoryId.DOCUMENT_REVIEW, maxFiles: 1, endpoints: ["pdf-comment-agent"], - operationConfig: pdfCommentAgentOperationConfig, + operationConfig: asRegistryConfig(pdfCommentAgentOperationConfig), automationSettings: null, synonyms: getSynonyms(t, "pdfCommentAgent"), versionStatus: "beta", diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 444ca380b3..d74cfbbc1d 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -214,10 +214,8 @@ export default defineConfig( ignores: [ "editor/src/core/components/**/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/contexts/**/*.{js,mjs,jsx,ts,tsx}", - "editor/src/core/data/**/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/hooks/**/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/services/**/*.{js,mjs,jsx,ts,tsx}", - "editor/src/core/tools/Automate.tsx", "editor/src/core/tools/annotate/useAnnotationSelection.ts", "editor/src/core/types/**/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/utils/**/*.{js,mjs,jsx,ts,tsx}",