Make Auto Rotate's settings editable in the pipeline composer (#7222)

Follow-up to #7152.

## Why

Auto Rotate was already selectable as a pipeline step —
`getExecutableTools` lists any registry tool with an `operationConfig`
and an endpoint resolvable from defaults, which it has. But once added,
the settings pane showed *"Displaying these tool params for editing is
not supported yet"* and the step card read *"Runs with default
settings"*.

Both come from the same cause. `classifyToolStepSupport` keys off the
operation config's mappers:

```js
const hasMappers = Boolean(config?.toApiParams && config?.fromApiParams);
if (!hasMappers) return "unsupported";
return entry.automationSettings ? "editable" : "noSettings";
```

Auto Rotate had `automationSettings` but no mappers, because its custom
processor builds its own FormData and never needed them — so it
classified as `unsupported`.

The more consequential half is `serializeToolStep`, which emits
`parameters: {}` for a mapper-less tool. A composed pipeline therefore
ran Auto Rotate on server defaults with no way to change that. "Runs
with default settings" was literal.

## What

Declares `toApiParams` / `fromApiParams` on `autoRotateOperationConfig`,
mapping `detectionMode`, `confidenceThreshold` and `inferUndetected`.

That flips the step to `editable`, so the composer reuses the existing
`AutoRotateAutomationSettings` panel, and the chosen settings now reach
the backend.

**No backend change.** Policy steps already serialise `parameters` as
form fields and the endpoint already accepts these three — verified
against a running server while working on #7152.

## Testing

- New round-trip test in `toolAutomation.test.ts`: the step serialises
to `{detectionMode, confidenceThreshold, inferUndetected}` and
deserialises back with `support: "editable"`, using the real operation
config.
- `core/hooks/tools/shared` + `portal/components/pipelines` suites: 63
tests pass.
- Typecheck across build variants, ESLint, Prettier.

The composer UI itself was not clicked: the portal is not served by the
editor dev server locally, so the verification is the unit test plus the
render path (`PipelineStepSettings` renders `entry.automationSettings`
when support is `editable`, and the registry entry supplies it). Worth a
click-through on the preview deploy.

## Note for maintainers

While tracing this I found `PIPELINE_OPERATIONS` in
`portal/components/pipelines/pipelineOperations.ts` is exported but
never imported — the composer builds its list from the tool registry
instead. `humanizeOperation` in the same file *is* still used as a label
fallback. Left alone here as it is out of scope, but it looks like dead
code worth deleting separately.

Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
ConnorYoh
2026-07-31 13:52:44 +00:00
committed by GitHub
parent 6933d11bce
commit 6094040e0a
2 changed files with 75 additions and 1 deletions
@@ -16,12 +16,43 @@ import {
ToolOperationHook,
} from "@app/hooks/tools/shared/useToolOperation";
import { createStandardErrorHandler } from "@app/utils/toolErrorHandler";
import type {
ToolApiParams,
ToolEndpoint,
} from "@app/hooks/tools/shared/toolApiMapping";
import {
AutoRotateParameters,
AutoRotateDetectionMode,
defaultParameters,
} from "@app/hooks/tools/autoRotate/useAutoRotateParameters";
export const AUTO_ROTATE_ENDPOINT = "/api/v1/misc/auto-rotate-pdf";
export const AUTO_ROTATE_ENDPOINT =
"/api/v1/misc/auto-rotate-pdf" satisfies ToolEndpoint;
type AutoRotateApiParams = ToolApiParams[typeof AUTO_ROTATE_ENDPOINT];
/**
* Frontend params -> the endpoint's request model. Declaring these is what lets a
* backend pipeline step carry this tool's settings: serializeToolStep sends `{}`
* for a tool without mappers, and the pipeline composer can only offer a settings
* panel for a tool that can map its parameters both ways.
*/
export const autoRotateToApiParams = (
parameters: AutoRotateParameters,
): AutoRotateApiParams => ({
detectionMode: parameters.detectionMode,
confidenceThreshold: parameters.confidenceThreshold,
inferUndetected: parameters.inferUndetected,
});
/** Rehydrate this tool's settings from a stored step's request body. */
export const autoRotateFromApiParams = (
apiParams: AutoRotateApiParams,
): Partial<AutoRotateParameters> => ({
detectionMode: apiParams.detectionMode as AutoRotateDetectionMode,
confidenceThreshold: apiParams.confidenceThreshold,
inferUndetected: apiParams.inferUndetected,
});
export type AutoRotateMethod = "text" | "osd" | "inferred" | "none";
@@ -135,6 +166,8 @@ export const autoRotateOperationConfig = defineCustomTool<AutoRotateParameters>(
operationType: "autoRotate",
endpoint: AUTO_ROTATE_ENDPOINT,
customProcessor: createAutoRotateProcessor(),
toApiParams: autoRotateToApiParams,
fromApiParams: autoRotateFromApiParams,
defaultParameters,
},
);
@@ -22,6 +22,8 @@ import { defaultParameters as compressDefaults } from "@app/hooks/tools/compress
import { splitOperationConfig } from "@app/hooks/tools/split/useSplitOperation";
import { SPLIT_METHODS } from "@app/constants/splitConstants";
import { redactOperationConfig } from "@app/hooks/tools/redact/useRedactOperation";
import { autoRotateOperationConfig } from "@app/hooks/tools/autoRotate/useAutoRotateOperation";
import { defaultParameters as autoRotateDefaults } from "@app/hooks/tools/autoRotate/useAutoRotateParameters";
function entry(over: Partial<ToolRegistryEntry>): ToolRegistryEntry {
return {
@@ -92,6 +94,11 @@ const dynamicRegistry: Partial<ToolRegistry> = {
automationSettings: NoopSettings,
operationConfig: asRegistryConfig(redactOperationConfig),
}),
autoRotate: entry({
name: "Auto Rotate",
automationSettings: NoopSettings,
operationConfig: asRegistryConfig(autoRotateOperationConfig),
}),
};
describe("getExecutableTools", () => {
@@ -157,6 +164,40 @@ describe("serialize/deserialize round-trip", () => {
});
});
test("auto rotate carries its detection settings into the backend step", () => {
// A custom-processor tool: the browser applies the rotations, but a backend
// pipeline still has to receive the detection settings, which only happens
// because the config declares mappers.
const step: WorkingToolStep = {
toolId: "autoRotate" as ToolId,
operation: "/api/v1/misc/auto-rotate-pdf",
params: {
...autoRotateDefaults,
detectionMode: "osd",
confidenceThreshold: 20,
inferUndetected: false,
},
support: "editable",
};
const api = serializeToolStep(step, dynamicRegistry);
expect(api.operation).toBe("/api/v1/misc/auto-rotate-pdf");
expect(api.parameters).toEqual({
detectionMode: "osd",
confidenceThreshold: 20,
inferUndetected: false,
});
const back = deserializeToolStep(api, dynamicRegistry);
expect(back.toolId).toBe("autoRotate");
expect(back.support).toBe("editable");
expect(back.params).toMatchObject({
detectionMode: "osd",
confidenceThreshold: 20,
inferUndetected: false,
});
});
test("a dynamic-endpoint tool (split by chapters) round-trips as an editable step", () => {
const step: WorkingToolStep = {
toolId: "split" as ToolId,