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<string, unknown)`, so on the way back out they can more safely
be cast back to their correct type when known.

One consequence of this is that I had to redesign the way we
special-case the Convert tool, which previously was a different shape
than all the other param types. Now it's just got optional parameters on
it, which isn't quite as type-safe as before, but it does mean all tools
are a consistent shape now, which I think is worth the tradeoff.
This commit is contained in:
James Brunton
2026-06-23 15:44:52 +00:00
committed by GitHub
parent 8e485801c9
commit 41181c9da1
12 changed files with 187 additions and 127 deletions
@@ -17,16 +17,16 @@ import CloseIcon from "@mui/icons-material/Close";
import WarningIcon from "@mui/icons-material/Warning";
import { ToolRegistry } from "@app/data/toolsTaxonomy";
import { ToolId } from "@app/types/toolId";
import { getAvailableToExtensions } from "@app/utils/convertUtils";
import { ErasedToolParams } from "@app/hooks/tools/shared/toolOperationTypes";
interface ToolConfigurationModalProps {
opened: boolean;
tool: {
id: string;
operation: string;
name: string;
parameters?: any;
parameters?: ErasedToolParams;
};
onSave: (parameters: any) => void;
onSave: (parameters: ErasedToolParams) => void;
onCancel: () => void;
toolRegistry: Partial<ToolRegistry>;
}
@@ -40,7 +40,7 @@ export default function ToolConfigurationModal({
}: ToolConfigurationModalProps) {
const { t } = useTranslation();
const [parameters, setParameters] = useState<any>({});
const [parameters, setParameters] = useState<ErasedToolParams>({});
// 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 (
<SettingsComponent
parameters={parameters}
onParameterChange={(key: string, value: any) => {
setParameters((prev: any) => ({ ...prev, [key]: value }));
}}
getAvailableToExtensions={getAvailableToExtensions}
selectedFiles={[]}
disabled={false}
/>
);
}
return (
<SettingsComponent
parameters={parameters}
onParameterChange={(key: string, value: any) => {
setParameters((prev: any) => ({ ...prev, [key]: value }));
onParameterChange={(key, value) => {
setParameters((prev) => ({ ...prev, [key]: value }));
}}
disabled={false}
/>
@@ -18,7 +18,7 @@ interface ToolListProps {
onToolConfigure: (index: number) => void;
onToolAdd: () => void;
getToolName: (operation: string) => string;
getToolDefaultParameters: (operation: string) => Record<string, any>;
getToolDefaultParameters: (operation: string) => Record<string, unknown>;
}
export default function ToolList({
@@ -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();
@@ -10,14 +10,20 @@ export interface FilesToolStepProps {
minFiles?: number;
}
export function createFilesToolStep(
interface StepBaseProps {
isVisible?: boolean;
isCollapsed?: boolean;
onCollapsedClick?: () => void;
}
export function createFilesToolStep<T>(
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"),
{
+12 -5
View File
@@ -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<any>;
// Settings component for automation configuration
automationSettings: React.ComponentType<any> | null;
// Operation configuration for automation. TParams is erased at the registry
// boundary; tools are authored type-safely via defineToolAutomation.
operationConfig?: ToolOperationConfig<ErasedToolParams>;
// Settings component for automation configuration.
automationSettings: React.ComponentType<
ToolAutomationSettingsProps<ErasedToolParams>
> | null;
// Whether this tool supports automation (defaults to true)
supportsAutomate?: boolean;
// Synonyms for search (optional)
@@ -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"),
@@ -40,7 +40,7 @@ export function useAutomationForm({
);
const getToolDefaultParameters = useCallback(
(operation: string): Record<string, any> => {
(operation: string): Record<string, unknown> => {
const config = toolRegistry[operation as ToolId]?.operationConfig;
if (config?.defaultParameters) {
return { ...config.defaultParameters };
@@ -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<TParams = void> =
| MultiFileToolOperationConfig<TParams>
| CustomToolOperationConfig<TParams>;
/**
* 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<TParams> {
parameters: TParams;
onParameterChange: <K extends keyof TParams>(
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<string, unknown>;
export type RegistryToolOperationConfig = ToolOperationConfig<ErasedToolParams>;
export type RegistryAutomationSettings = ComponentType<
ToolAutomationSettingsProps<ErasedToolParams>
> | null;
/**
* Store a tool's typed operationConfig in the registry. The input is validated as
* a real ToolOperationConfig<TParams>, 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<TParams>(
config: ToolOperationConfig<TParams>,
): 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<TParams> (inferred
* from the module), then erased to the registry's shared props shape.
*/
export function lazySettings<TParams>(
loader: () => Promise<{
default: ComponentType<ToolAutomationSettingsProps<TParams>>;
}>,
): RegistryAutomationSettings {
return lazy(loader) as unknown as RegistryAutomationSettings;
}
/**
* Complete tool operation interface returned by useToolOperation.
*/
+5 -2
View File
@@ -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<MiddleStepConfig, "title" | "content">,
content?: React.ReactNode,
) => ({
title,
@@ -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<ErasedToolParams>,
parameters: ErasedToolParams,
files: File[],
filePrefix: string,
): Promise<File[]> => {
@@ -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<ErasedToolParams>,
parameters: ErasedToolParams,
files: File[],
filePrefix: string,
): Promise<File[]> => {
@@ -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<File[]> => {
@@ -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,
@@ -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",
-2
View File
@@ -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}",