Fix more any typing usage in the frontend (#6664)

# Description of Changes
Continued effort to remove the remaining uses of the `any` type from our
TS code. The vast majority of these uses that it cleans up was just
catching errors as `any`, which are pretty simple to fix. I couldn't
completely remove the `any` type usage from `core/tools` because there
were cascading issues from a couple of the files in there (most notably
Automate) but still, moving in the right direction.
This commit is contained in:
James Brunton
2026-06-19 13:37:53 +00:00
committed by GitHub
parent 3793a6df52
commit 6a9876a067
13 changed files with 174 additions and 83 deletions
@@ -20,12 +20,32 @@ import PhotoCameraRoundedIcon from "@mui/icons-material/PhotoCameraRounded";
import UploadRoundedIcon from "@mui/icons-material/UploadRounded";
import AddPhotoAlternateRoundedIcon from "@mui/icons-material/AddPhotoAlternateRounded";
import CheckCircleRoundedIcon from "@mui/icons-material/CheckCircleRounded";
import { loadJscanify } from "@app/utils/loadJscanify";
import {
loadJscanify,
type JscanifyCornerPoints,
type JscanifyScanner,
} from "@app/utils/loadJscanify";
import apiClient from "@app/services/apiClient";
// Use the configured API base (e.g. api.stirling.com), not the page origin.
const API_BASE = (apiClient.defaults.baseURL ?? "").replace(/\/+$/, "");
// Experimental camera controls (W3C Image Capture / MediaStream extensions) that
// are not yet part of the standard DOM lib typings but are widely shipped on
// mobile browsers and required for document scanning.
declare global {
interface MediaTrackCapabilities {
focusMode?: string[];
exposureMode?: string[];
torch?: boolean;
}
interface MediaTrackConstraintSet {
focusMode?: ConstrainDOMString;
exposureMode?: ConstrainDOMString;
torch?: ConstrainBoolean;
}
}
/**
* MobileScannerPage
*
@@ -63,7 +83,7 @@ export default function MobileScannerPage() {
const highlightCanvasRef = useRef<HTMLCanvasElement>(null);
const streamRef = useRef<MediaStream | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const scannerRef = useRef<any>(null);
const scannerRef = useRef<JscanifyScanner | null>(null);
const highlightIntervalRef = useRef<number | null>(null);
// Detection resolution - extremely low for mobile performance
@@ -254,15 +274,15 @@ export default function MobileScannerPage() {
// Configure camera capabilities for document scanning
try {
const capabilities = videoTrack.getCapabilities() as any; // Cast to any for experimental camera APIs
const constraints: any = { advanced: [] };
const capabilities = videoTrack.getCapabilities();
const advanced: MediaTrackConstraintSet[] = [];
// 1. Enable continuous autofocus
if (
capabilities.focusMode &&
capabilities.focusMode.includes("continuous")
) {
constraints.advanced.push({ focusMode: "continuous" });
advanced.push({ focusMode: "continuous" });
console.log("✓ Continuous autofocus enabled");
}
@@ -271,7 +291,7 @@ export default function MobileScannerPage() {
capabilities.exposureMode &&
capabilities.exposureMode.includes("continuous")
) {
constraints.advanced.push({ exposureMode: "continuous" });
advanced.push({ exposureMode: "continuous" });
console.log("✓ Auto-exposure enabled");
}
@@ -282,8 +302,8 @@ export default function MobileScannerPage() {
}
// Apply all constraints
if (constraints.advanced.length > 0) {
await videoTrack.applyConstraints(constraints);
if (advanced.length > 0) {
await videoTrack.applyConstraints({ advanced });
}
} catch (err) {
console.log("Could not configure camera features:", err);
@@ -444,15 +464,19 @@ export default function MobileScannerPage() {
// Step 2: Simple jscanify detection
const detectionStart = performance.now();
let corners = null;
let corners: JscanifyCornerPoints | null = null;
// Run jscanify detection directly - convert canvas to Mat first
const mat = (window as any).cv.imread(detectionCanvas);
const contour = scannerRef.current.findPaperContour(mat);
mat.delete();
const cv = window.cv;
const scanner = scannerRef.current;
if (cv && scanner) {
const mat = cv.imread(detectionCanvas);
const contour = scanner.findPaperContour(mat);
mat.delete();
if (contour) {
corners = scannerRef.current.getCornerPoints(contour);
if (contour) {
corners = scanner.getCornerPoints(contour);
}
}
const detectionTime = performance.now() - detectionStart;
@@ -660,7 +684,9 @@ export default function MobileScannerPage() {
let finalDataUrl: string;
// Apply jscanify processing if enabled and available
if (autoEnhance && scannerRef.current && openCvReady) {
const cv = window.cv;
const scanner = scannerRef.current;
if (autoEnhance && scanner && openCvReady && cv) {
try {
// Create low-res canvas for detection (faster processing)
const detectionCanvas = document.createElement("canvas");
@@ -683,11 +709,11 @@ export default function MobileScannerPage() {
);
// Run detection on low-res image
const mat = (window as any).cv.imread(detectionCanvas);
const contour = scannerRef.current.findPaperContour(mat);
const mat = cv.imread(detectionCanvas);
const contour = scanner.findPaperContour(mat);
if (contour) {
const cornerPoints = scannerRef.current.getCornerPoints(contour);
const cornerPoints = scanner.getCornerPoints(contour);
// Scale corner points back to full resolution
if (cornerPoints) {
@@ -746,7 +772,7 @@ export default function MobileScannerPage() {
const docHeight = Math.round((leftHeight + rightHeight) / 2);
// Extract paper from full-resolution canvas with scaled corner points
const resultCanvas = scannerRef.current.extractPaper(
const resultCanvas = scanner.extractPaper(
canvas,
docWidth,
docHeight,
@@ -891,8 +917,8 @@ export default function MobileScannerPage() {
try {
const videoTrack = streamRef.current.getVideoTracks()[0];
await videoTrack.applyConstraints({
advanced: [{ torch: !torchEnabled } as any], // Cast to any for experimental torch API
} as any);
advanced: [{ torch: !torchEnabled }],
});
setTorchEnabled(!torchEnabled);
console.log("Torch:", !torchEnabled ? "ON" : "OFF");
} catch (err) {
@@ -123,7 +123,7 @@ describe("Convert Tool Integration Tests", () => {
beforeEach(() => {
vi.clearAllMocks();
// Setup default apiClient mock
mockedApiClient.post = vi.fn() as any;
mockedApiClient.post = vi.fn() as typeof mockedApiClient.post;
});
afterEach(() => {
@@ -83,7 +83,10 @@ async function getIDBFolders(
const all = tx.objectStore(storeName).getAll();
all.onsuccess = () =>
resolve(
(all.result || []).map((f: any) => ({ id: f.id, name: f.name })),
(all.result || []).map((f: { id: string; name: string }) => ({
id: f.id,
name: f.name,
})),
);
all.onerror = () => resolve([]);
};
@@ -275,7 +278,7 @@ test.describe("Watched Folders — Create / Edit / Delete", () => {
dbName: string,
storeName: string,
key: string,
value: any,
value: unknown,
) =>
new Promise<void>((resolve) => {
const req = indexedDB.open(dbName);
@@ -1,7 +1,10 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useViewScopedFiles } from "@app/hooks/tools/shared/useViewScopedFiles";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import {
createToolFlow,
type MiddleStepConfig,
} from "@app/components/tools/shared/createToolFlow";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useEndpointEnabled } from "@app/hooks/useEndpointConfig";
import { useAddAttachmentsParameters } from "@app/hooks/tools/addAttachments/useAddAttachmentsParameters";
@@ -36,9 +39,9 @@ const AddAttachments = ({
if (operation.files && onComplete) {
onComplete(operation.files);
}
} catch (error: any) {
} catch (error) {
onError?.(
error?.message ||
(error instanceof Error ? error.message : undefined) ||
t(
"AddAttachmentsRequest.error.failed",
"Add attachments operation failed",
@@ -70,7 +73,7 @@ const AddAttachments = ({
});
const getSteps = () => {
const steps: any[] = [];
const steps: MiddleStepConfig[] = [];
// Step 1: Attachments Selection
steps.push({
@@ -1,7 +1,10 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useViewScopedFiles } from "@app/hooks/tools/shared/useViewScopedFiles";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import {
createToolFlow,
type MiddleStepConfig,
} from "@app/components/tools/shared/createToolFlow";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useEndpointEnabled } from "@app/hooks/useEndpointConfig";
import { useAddPageNumbersParameters } from "@app/components/tools/addPageNumbers/useAddPageNumbersParameters";
@@ -35,9 +38,9 @@ const AddPageNumbers = ({
if (operation.files && onComplete) {
onComplete(operation.files);
}
} catch (error: any) {
} catch (error) {
onError?.(
error?.message ||
(error instanceof Error ? error.message : undefined) ||
t("addPageNumbers.error.failed", "Add page numbers operation failed"),
);
}
@@ -67,7 +70,7 @@ const AddPageNumbers = ({
});
const getSteps = () => {
const steps: any[] = [];
const steps: MiddleStepConfig[] = [];
// Step 1: Position Selection & Pages/Starting Number
steps.push({
+7 -4
View File
@@ -1,6 +1,9 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import {
createToolFlow,
type MiddleStepConfig,
} from "@app/components/tools/shared/createToolFlow";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useEndpointEnabled } from "@app/hooks/useEndpointConfig";
import { useViewScopedFiles } from "@app/hooks/tools/shared/useViewScopedFiles";
@@ -41,9 +44,9 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
if (operation.files && onComplete) {
onComplete(operation.files);
}
} catch (error: any) {
} catch (error) {
onError?.(
error?.message ||
(error instanceof Error ? error.message : undefined) ||
t("AddStampRequest.error.failed", "Add stamp operation failed"),
);
}
@@ -73,7 +76,7 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
});
const getSteps = () => {
const steps: any[] = [];
const steps: MiddleStepConfig[] = [];
// Step 1: Stamp Setup
steps.push({
@@ -26,6 +26,7 @@ import {
useNavigationState,
} from "@app/contexts/NavigationContext";
import { useFileSelection } from "@app/contexts/FileContext";
import { isStirlingFile } from "@app/types/fileContext";
const extractBookmarks = async (file: File): Promise<BookmarkPayload[]> => {
const formData = new FormData();
@@ -39,7 +40,7 @@ const extractBookmarks = async (file: File): Promise<BookmarkPayload[]> => {
return response.data as BookmarkPayload[];
};
const useStableCallback = <T extends (...args: any[]) => any>(
const useStableCallback = <T extends (...args: never[]) => unknown>(
callback: T,
): T => {
const callbackRef = useRef(callback);
@@ -112,7 +113,7 @@ const EditTableOfContents = (props: BaseToolProps) => {
const payload = await extractBookmarks(file);
const bookmarks = hydrateBookmarkPayload(payload);
setBookmarks(bookmarks);
setLastLoadedFileId((file as any)?.fileId ?? file.name);
setLastLoadedFileId(isStirlingFile(file) ? file.fileId : file.name);
if (showToast) {
alert({
@@ -164,7 +165,7 @@ const EditTableOfContents = (props: BaseToolProps) => {
return;
}
const fileId = (selectedFile as any)?.fileId ?? selectedFile.name;
const fileId = selectedFile.fileId;
if (fileId === lastLoadedFileId) {
return;
}
@@ -466,6 +467,7 @@ const EditTableOfContents = (props: BaseToolProps) => {
});
};
(EditTableOfContents as any).tool = () => useEditTableOfContentsOperation;
(EditTableOfContents as ToolComponent).tool = () =>
useEditTableOfContentsOperation;
export default EditTableOfContents as ToolComponent;
@@ -34,9 +34,9 @@ const ReorganizePages = ({
if (operation.files && onComplete) {
onComplete(operation.files);
}
} catch (error: any) {
} catch (error) {
onError?.(
error?.message ||
(error instanceof Error ? error.message : undefined) ||
t("reorganizePages.error.failed", "Failed to reorganize pages"),
);
}
@@ -107,6 +107,6 @@ const ReorganizePages = ({
});
};
(ReorganizePages as any).tool = () => useReorganizePagesOperation;
(ReorganizePages as ToolComponent).tool = () => useReorganizePagesOperation;
export default ReorganizePages as ToolComponent;
@@ -28,6 +28,7 @@ import {
ActionIcon,
} from "@mantine/core";
import { useTranslation } from "react-i18next";
import { isAxiosError } from "axios";
import {
useFormFill,
useAllFormValues,
@@ -267,13 +268,15 @@ const FormFill = (_props: BaseToolProps) => {
detail: { blob: filledBlob },
});
window.dispatchEvent(event);
} catch (err: any) {
} catch (err) {
const status = isAxiosError(err) ? err.response?.status : undefined;
const message =
err?.response?.status === 413
status === 413
? "File too large. Try reducing the PDF size first."
: err?.response?.status === 400
: status === 400
? "Invalid form data. Please check all fields."
: err?.message || "Failed to save filled form";
: (err instanceof Error ? err.message : undefined) ||
"Failed to save filled form";
setSaveError(message);
console.error("[FormFill] Save failed:", err);
} finally {
@@ -30,6 +30,7 @@ import React, {
useSyncExternalStore,
} from "react";
import { useDebouncedCallback } from "@mantine/hooks";
import { isAxiosError } from "axios";
import type {
FormField,
FormFillState,
@@ -361,11 +362,13 @@ export function FormFillProvider({
forFileIdRef.current = fileId ?? null;
setForFileId(fileId ?? null);
dispatch({ type: "FETCH_SUCCESS", fields });
} catch (err: any) {
} catch (err) {
if (fetchVersionRef.current !== version) return; // stale
const msg =
err?.response?.data?.message ||
err?.message ||
(isAxiosError<{ message?: string }>(err)
? err.response?.data?.message
: undefined) ||
(err instanceof Error ? err.message : undefined) ||
"Failed to fetch form fields";
dispatch({ type: "FETCH_ERROR", error: msg });
}
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
import { useTranslation } from "react-i18next";
import { isAxiosError } from "axios";
import DescriptionIcon from "@mui/icons-material/DescriptionOutlined";
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
@@ -16,6 +17,7 @@ import {
import { useViewer } from "@app/contexts/ViewerContext";
import { createStirlingFilesAndStubs } from "@app/services/fileStubHelpers";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import type { FileId } from "@app/types/file";
import { getDefaultWorkbench } from "@app/types/workbench";
import { CONVERSION_ENDPOINTS } from "@app/constants/convertConstants";
import apiClient from "@app/services/apiClient";
@@ -294,7 +296,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
const imagesByPageRef = useRef<PdfJsonImageElement[][]>([]);
const lastLoadedFileRef = useRef<File | null>(null);
const autoLoadKeyRef = useRef<string | null>(null);
const sourceFileIdRef = useRef<string | null>(null);
const sourceFileIdRef = useRef<FileId | null>(null);
const loadRequestIdRef = useRef(0);
const latestPdfRequestIdRef = useRef<number | null>(null);
const loadedDocumentRef = useRef<PdfJsonDocument | null>(null);
@@ -339,8 +341,8 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
};
}, []);
const isCacheUnavailableError = useCallback((error: any): boolean => {
const status = error?.response?.status;
const isCacheUnavailableError = useCallback((error: unknown): boolean => {
const status = isAxiosError(error) ? error.response?.status : undefined;
// Treat any 410 as cache unavailable, since responseType: 'blob' makes
// it impossible to reliably check the JSON body
return status === 410;
@@ -804,14 +806,20 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
} else {
console.log("Job not complete yet, continuing to poll...");
}
} catch (pollError: any) {
} catch (pollError) {
console.error("Error polling job status:", pollError);
const status = isAxiosError(pollError)
? pollError.response?.status
: undefined;
console.error("Poll error details:", {
status: pollError?.response?.status,
data: pollError?.response?.data,
message: pollError?.message,
status,
data: isAxiosError(pollError)
? pollError.response?.data
: undefined,
message:
pollError instanceof Error ? pollError.message : undefined,
});
if (pollError?.response?.status === 404) {
if (status === 404) {
throw new Error("Job not found on server", {
cause: pollError,
});
@@ -864,12 +872,12 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
cachedJobIdRef.current = newJobId;
setFileName(file.name);
setErrorMessage(null);
} catch (error: any) {
} catch (error) {
console.error("Failed to load file", error);
console.error("Error details:", {
message: error?.message,
response: error?.response?.data,
stack: error?.stack,
message: error instanceof Error ? error.message : undefined,
response: isAxiosError(error) ? error.response?.data : undefined,
stack: error instanceof Error ? error.stack : undefined,
});
if (loadRequestIdRef.current !== requestId) {
@@ -885,7 +893,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
if (isPdf) {
const errorMsg =
error?.message ||
(error instanceof Error ? error.message : undefined) ||
t(
"pdfTextEditor.conversionFailed",
"Failed to convert PDF. Please try again.",
@@ -1406,11 +1414,11 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
onComplete([pdfFile]);
}
setErrorMessage(null);
} catch (error: any) {
} catch (error) {
console.error("Failed to convert JSON back to PDF", error);
const message =
error?.response?.data ||
error?.message ||
(isAxiosError(error) ? error.response?.data : undefined) ||
(error instanceof Error ? error.message : undefined) ||
t(
"pdfTextEditor.errors.pdfConversion",
"Unable to convert the edited JSON back into a PDF.",
@@ -1451,9 +1459,8 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
return;
}
const parentStub = selectors.getStirlingFileStub(
sourceFileIdRef.current as any,
);
const sourceFileId = sourceFileIdRef.current;
const parentStub = selectors.getStirlingFileStub(sourceFileId);
if (!parentStub) {
console.warn(
"[PdfTextEditor] Could not find parent stub for save to workbench",
@@ -1660,11 +1667,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
);
// Replace the original file with the edited version
await consumeFiles(
[sourceFileIdRef.current as any],
stirlingFiles,
stubs,
);
await consumeFiles([sourceFileId], stirlingFiles, stubs);
// Update the source file ID to point to the new file
sourceFileIdRef.current = stubs[0].id;
@@ -1676,11 +1679,11 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
// Set flag to trigger navigation after state update is processed
setShouldNavigateAfterSave(true);
} catch (error: any) {
} catch (error) {
console.error("Failed to save to workbench", error);
const message =
error?.response?.data ||
error?.message ||
(isAxiosError(error) ? error.response?.data : undefined) ||
(error instanceof Error ? error.message : undefined) ||
t(
"pdfTextEditor.errors.pdfConversion",
"Unable to save changes to workbench.",
@@ -1955,7 +1958,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
autoLoadKeyRef.current = fileKey;
// Capture the source file ID for save-to-workbench functionality
sourceFileIdRef.current = (autoLoadFile as any).fileId ?? null;
sourceFileIdRef.current = autoLoadFile.fileId ?? null;
void handleLoadFile(autoLoadFile);
}, [autoLoadFile, navigationState.selectedTool, handleLoadFile]);
+45 -2
View File
@@ -1,9 +1,52 @@
import { withBasePath } from "@app/constants/app";
/** A single point in image space, as returned by jscanify corner detection. */
export interface JscanifyPoint {
x: number;
y: number;
}
/** The four detected document corners returned by {@link JscanifyScanner.getCornerPoints}. */
export interface JscanifyCornerPoints {
topLeftCorner: JscanifyPoint;
topRightCorner: JscanifyPoint;
bottomLeftCorner: JscanifyPoint;
bottomRightCorner: JscanifyPoint;
}
/** Minimal subset of an OpenCV.js `Mat` that this app interacts with directly. */
export interface OpenCVMat {
delete(): void;
}
/** Minimal subset of the OpenCV.js runtime exposed on `window.cv`. */
export interface OpenCV {
/** Defined only once the WASM runtime has finished initializing. */
readonly Mat: unknown;
imread(source: HTMLImageElement | HTMLCanvasElement | string): OpenCVMat;
}
/** The jscanify scanner instance API used by the mobile scanner. */
export interface JscanifyScanner {
findPaperContour(image: OpenCVMat): OpenCVMat | undefined;
getCornerPoints(contour: OpenCVMat): JscanifyCornerPoints;
extractPaper(
image: HTMLCanvasElement,
resultWidth: number,
resultHeight: number,
cornerPoints?: JscanifyCornerPoints,
): HTMLCanvasElement;
}
/** Constructor for jscanify, exposed on `window.jscanify`. */
export interface JscanifyConstructor {
new (): JscanifyScanner;
}
declare global {
interface Window {
cv?: any;
jscanify?: any;
cv?: OpenCV;
jscanify?: JscanifyConstructor;
}
}
+2 -3
View File
@@ -215,10 +215,9 @@ export default defineConfig(
"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/pages/**/*.{js,mjs,jsx,ts,tsx}",
"editor/src/core/services/**/*.{js,mjs,jsx,ts,tsx}",
"editor/src/core/tests/**/*.{js,mjs,jsx,ts,tsx}",
"editor/src/core/tools/**/*.{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}",
],