Fix more any types

This commit is contained in:
James Brunton
2025-11-06 13:47:21 +00:00
parent aaf6c30413
commit df6e367881
6 changed files with 133 additions and 71 deletions

View File

@@ -1,3 +1,4 @@
import type { CSSProperties } from 'react';
import type { AddStampParameters } from '@app/components/tools/addStamp/useAddStampParameters';
export type ContainerSize = { width: number; height: number };
@@ -48,8 +49,6 @@ export const getFirstSelectedPage = (input: string): number => {
return 1;
};
export type StampPreviewStyle = { container: any; item: any };
// Unified per-alphabet preview adjustments
export type Alphabet = 'roman' | 'arabic' | 'japanese' | 'korean' | 'chinese' | 'thai';
export type AlphabetTweaks = { scale: number; rowOffsetRem: [number, number, number]; lineHeight: number; capHeightRatio: number; defaultFontSize: number };
@@ -62,11 +61,19 @@ export const ALPHABET_PREVIEW_TWEAKS: Record<Alphabet, AlphabetTweaks> = {
chinese: { scale: 1/1.2, rowOffsetRem: [0, 2, 2.8], lineHeight: 1, capHeightRatio: 0.72, defaultFontSize: 30 }, // temporary default font size so that it fits on the PDF
thai: { scale: 1/1.2, rowOffsetRem: [-1, 0, .8], lineHeight: 1, capHeightRatio: 0.66, defaultFontSize: 80 },
};
export const getAlphabetPreviewScale = (alphabet: string): number => (ALPHABET_PREVIEW_TWEAKS as any)[alphabet]?.scale ?? 1.0;
const getAlphabetTweaks = (alphabet: string): AlphabetTweaks | undefined =>
ALPHABET_PREVIEW_TWEAKS[alphabet as Alphabet];
export const getDefaultFontSizeForAlphabet = (alphabet: string): number => {
return (ALPHABET_PREVIEW_TWEAKS as any)[alphabet]?.defaultFontSize ?? 80;
};
export const getAlphabetPreviewScale = (alphabet: string): number =>
getAlphabetTweaks(alphabet)?.scale ?? 1.0;
export const getDefaultFontSizeForAlphabet = (alphabet: string): number =>
getAlphabetTweaks(alphabet)?.defaultFontSize ?? 80;
export interface StampPreviewStyle {
container: CSSProperties;
item: CSSProperties;
}
export function computeStampPreviewStyle(
parameters: AddStampParameters,
@@ -83,7 +90,7 @@ export function computeStampPreviewStyle(
const heightPts = pageSize?.heightPts ?? 841.89; // A4 height at 72 DPI
const scaleX = pageWidthPx / widthPts;
const scaleY = pageHeightPx / heightPts;
if (pageWidthPx <= 0 || pageHeightPx <= 0) return { item: {}, container: {} } as any;
if (pageWidthPx <= 0 || pageHeightPx <= 0) return { item: {}, container: {} };
const marginPts = (widthPts + heightPts) / 2 * (marginFactorMap[parameters.customMargin] ?? 0.035);
@@ -110,24 +117,15 @@ export function computeStampPreviewStyle(
// Convert measured px width back to PDF points using horizontal scale
widthPtsContent = measuredWidthPx / scaleX;
let adjustmentFactor = 1.0;
switch (parameters.alphabet) {
case 'roman':
adjustmentFactor = 0.90;
break;
case 'arabic':
case 'thai':
adjustmentFactor = 0.92;
break;
case 'japanese':
case 'korean':
case 'chinese':
adjustmentFactor = 0.88;
break;
default:
adjustmentFactor = 0.93;
}
widthPtsContent *= adjustmentFactor;
const adjustments: Partial<Record<AddStampParameters['alphabet'], number>> = {
roman: 0.90,
arabic: 0.92,
thai: 0.92,
japanese: 0.88,
korean: 0.88,
chinese: 0.88,
};
widthPtsContent *= adjustments[parameters.alphabet] ?? 0.93;
}
}
@@ -150,7 +148,7 @@ export function computeStampPreviewStyle(
if (parameters.overrideX >= 0 && parameters.overrideY >= 0) return parameters.overrideY;
// For text, backend positions using cap height, not full font size
const heightForY = parameters.stampType === 'text'
? heightPtsContent * ((ALPHABET_PREVIEW_TWEAKS as any)[parameters.alphabet]?.capHeightRatio ?? 0.70)
? heightPtsContent * (getAlphabetTweaks(parameters.alphabet)?.capHeightRatio ?? 0.70)
: heightPtsContent;
switch (Math.floor((position - 1) / 3)) {
case 0: // Top
@@ -172,12 +170,11 @@ export function computeStampPreviewStyle(
try {
const rootFontSizePx = parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16;
const rowIndex = Math.floor((position - 1) / 3); // 0 top, 1 middle, 2 bottom
const offsets = (ALPHABET_PREVIEW_TWEAKS as any)[parameters.alphabet]?.rowOffsetRem ?? [0, 0, 0];
const offsets = getAlphabetTweaks(parameters.alphabet)?.rowOffsetRem ?? [0, 0, 0];
const offsetRem = offsets[rowIndex] ?? 0;
yPx += offsetRem * rootFontSizePx;
} catch (e) {
// no-op
console.error(e);
} catch (error) {
console.error(error);
}
}
const widthPx = widthPtsContent * scaleX;
@@ -226,7 +223,7 @@ export function computeStampPreviewStyle(
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-start',
lineHeight: (ALPHABET_PREVIEW_TWEAKS as any)[parameters.alphabet]?.lineHeight ?? 1,
lineHeight: getAlphabetTweaks(parameters.alphabet)?.lineHeight ?? 1,
alignItems,
cursor: showQuickGrid ? 'default' : 'move',
pointerEvents: showQuickGrid ? 'none' : 'auto',

View File

@@ -17,15 +17,31 @@ 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 type { AutomationParameters } from '@app/types/automation';
type BaseSettingsComponent = React.ComponentType<{
parameters: AutomationParameters;
onParameterChange: (key: string, value: unknown) => void;
disabled?: boolean;
}>;
type ConvertSettingsComponent = React.ComponentType<{
parameters: AutomationParameters;
onParameterChange: (key: string, value: unknown) => void;
getAvailableToExtensions: typeof getAvailableToExtensions;
selectedFiles: File[];
disabled?: boolean;
}>;
interface ToolConfigurationModalProps {
opened: boolean;
tool: {
id: string;
operation: string;
name: string;
parameters?: any;
parameters?: AutomationParameters;
};
onSave: (parameters: any) => void;
onSave: (parameters: AutomationParameters) => void;
onCancel: () => void;
toolRegistry: Partial<ToolRegistry>;
}
@@ -33,11 +49,11 @@ interface ToolConfigurationModalProps {
export default function ToolConfigurationModal({ opened, tool, onSave, onCancel, toolRegistry }: ToolConfigurationModalProps) {
const { t } = useTranslation();
const [parameters, setParameters] = useState<any>({});
const [parameters, setParameters] = useState<AutomationParameters>({});
// Get tool info from registry
const toolInfo = toolRegistry[tool.operation as ToolId];
const SettingsComponent = toolInfo?.automationSettings;
const SettingsComponent = toolInfo?.automationSettings as BaseSettingsComponent | ConvertSettingsComponent | undefined;
// Initialize parameters from tool (which should contain defaults from registry)
useEffect(() => {
@@ -49,6 +65,13 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel,
}
}, [tool.parameters, tool.operation]);
const updateParameter = (key: string, value: unknown) => {
setParameters(prev => ({
...prev,
[key]: value,
}));
};
// Render the settings component
const renderToolSettings = () => {
if (!SettingsComponent) {
@@ -63,12 +86,11 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel,
// Special handling for ConvertSettings which needs additional props
if (tool.operation === 'convert') {
const ConvertComponent = SettingsComponent as ConvertSettingsComponent;
return (
<SettingsComponent
<ConvertComponent
parameters={parameters}
onParameterChange={(key: string, value: any) => {
setParameters((prev: any) => ({ ...prev, [key]: value }));
}}
onParameterChange={updateParameter}
getAvailableToExtensions={getAvailableToExtensions}
selectedFiles={[]}
disabled={false}
@@ -76,12 +98,11 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel,
);
}
const GenericComponent = SettingsComponent as BaseSettingsComponent;
return (
<SettingsComponent
<GenericComponent
parameters={parameters}
onParameterChange={(key: string, value: any) => {
setParameters((prev: any) => ({ ...prev, [key]: value }));
}}
onParameterChange={updateParameter}
disabled={false}
/>
);

View File

@@ -2,8 +2,9 @@
* File lifecycle management - Resource cleanup and memory management
*/
import type { Dispatch, MutableRefObject } from 'react';
import { FileId } from '@app/types/file';
import { FileContextAction, StirlingFileStub, ProcessedFilePage } from '@app/types/fileContext';
import { FileContextAction, StirlingFileStub, ProcessedFilePage, FileContextState } from '@app/types/fileContext';
const DEBUG = process.env.NODE_ENV === 'development';
@@ -16,8 +17,8 @@ export class FileLifecycleManager {
private fileGenerations = new Map<string, number>(); // Generation tokens to prevent stale cleanup
constructor(
private filesRef: React.MutableRefObject<Map<FileId, File>>,
private dispatch: React.Dispatch<FileContextAction>
private filesRef: MutableRefObject<Map<FileId, File>>,
private dispatch: Dispatch<FileContextAction>
) {}
/**
@@ -34,7 +35,7 @@ export class FileLifecycleManager {
/**
* Clean up resources for a specific file (with stateRef access for complete cleanup)
*/
cleanupFile = (fileId: FileId, stateRef?: React.MutableRefObject<any>): void => {
cleanupFile = (fileId: FileId, stateRef?: MutableRefObject<FileContextState>): void => {
// Use comprehensive cleanup (same as removeFiles)
this.cleanupAllResourcesForFile(fileId, stateRef);
@@ -68,7 +69,7 @@ export class FileLifecycleManager {
/**
* Schedule delayed cleanup for a file with generation token to prevent stale cleanup
*/
scheduleCleanup = (fileId: FileId, delay: number = 30000, stateRef?: React.MutableRefObject<any>): void => {
scheduleCleanup = (fileId: FileId, delay: number = 30000, stateRef?: MutableRefObject<FileContextState>): void => {
// Cancel existing timer
const existingTimer = this.cleanupTimers.get(fileId);
if (existingTimer) {
@@ -101,7 +102,7 @@ export class FileLifecycleManager {
/**
* Remove a file immediately with complete resource cleanup
*/
removeFiles = (fileIds: FileId[], stateRef?: React.MutableRefObject<any>): void => {
removeFiles = (fileIds: FileId[], stateRef?: MutableRefObject<FileContextState>): void => {
fileIds.forEach(fileId => {
// Clean up all resources for this file
this.cleanupAllResourcesForFile(fileId, stateRef);
@@ -114,7 +115,7 @@ export class FileLifecycleManager {
/**
* Complete resource cleanup for a single file
*/
private cleanupAllResourcesForFile = (fileId: FileId, stateRef?: React.MutableRefObject<any>): void => {
private cleanupAllResourcesForFile = (fileId: FileId, stateRef?: MutableRefObject<FileContextState>): void => {
// Remove from files ref
this.filesRef.current.delete(fileId);
@@ -166,7 +167,7 @@ export class FileLifecycleManager {
/**
* Update file record with race condition guards
*/
updateStirlingFileStub = (fileId: FileId, updates: Partial<StirlingFileStub>, stateRef?: React.MutableRefObject<any>): void => {
updateStirlingFileStub = (fileId: FileId, updates: Partial<StirlingFileStub>, stateRef?: MutableRefObject<FileContextState>): void => {
// Guard against updating removed files (race condition protection)
if (!this.filesRef.current.has(fileId)) {
if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`);

View File

@@ -14,14 +14,15 @@ export interface ProcessedFilePage {
pageNumber?: number;
rotation?: number;
splitBefore?: boolean;
[key: string]: any;
splitAfter?: boolean;
originalPageNumber?: number;
}
export interface ProcessedFileMetadata {
pages: ProcessedFilePage[];
totalPages?: number;
lastProcessed?: number;
[key: string]: any;
thumbnailUrl?: string;
}
/**
@@ -81,8 +82,8 @@ export interface StirlingFile extends File {
// Type guard to check if a File object has an embedded fileId
export function isStirlingFile(file: File): file is StirlingFile {
return 'fileId' in file && typeof (file as any).fileId === 'string' &&
'quickKey' in file && typeof (file as any).quickKey === 'string';
const withIds = file as Partial<StirlingFile>;
return typeof withIds.fileId === 'string' && typeof withIds.quickKey === 'string';
}
// Create a StirlingFile from a regular File object
@@ -125,13 +126,16 @@ export function extractFiles(files: StirlingFile[]): File[] {
}
// Check if an object is a File or StirlingFile (replaces instanceof File checks)
export function isFileObject(obj: any): obj is File | StirlingFile {
return obj &&
typeof obj.name === 'string' &&
typeof obj.size === 'number' &&
typeof obj.type === 'string' &&
typeof obj.lastModified === 'number' &&
typeof obj.arrayBuffer === 'function';
export function isFileObject(obj: unknown): obj is File | StirlingFile {
if (!obj || typeof obj !== 'object') return false;
const candidate = obj as Partial<File>;
return (
typeof candidate.name === 'string' &&
typeof candidate.size === 'number' &&
typeof candidate.type === 'string' &&
typeof candidate.lastModified === 'number' &&
typeof candidate.arrayBuffer === 'function'
);
}

View File

@@ -1,4 +1,5 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import axios from 'axios';
import apiClient from '@app/services/apiClient';
// Retry configuration
@@ -12,6 +13,24 @@ function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
interface ErrorPayload {
message?: string;
error?: string;
}
function extractErrorMessage(error: unknown, fallback: string): string {
if (axios.isAxiosError<ErrorPayload>(error)) {
return error.response?.data?.message
|| error.response?.data?.error
|| error.message
|| fallback;
}
if (error instanceof Error) {
return error.message;
}
return fallback;
}
export interface AppConfig {
baseUrl?: string;
contextPath?: string;
@@ -80,21 +99,22 @@ export const AppConfigProvider: React.FC<{ children: React.ReactNode }> = ({ chi
console.log('[AppConfig] Successfully fetched app config');
setLoading(false);
return; // Success - exit function
} catch (err: any) {
const status = err?.response?.status;
} catch (error: unknown) {
const status = axios.isAxiosError(error) ? error.response?.status : undefined;
// Check if we should retry (network errors or 5xx errors)
const shouldRetry = (!status || status >= 500) && attempt < MAX_RETRIES;
if (shouldRetry) {
console.warn(`[AppConfig] Attempt ${attempt + 1} failed (status ${status || 'network error'}):`, err.message, '- will retry...');
const message = extractErrorMessage(error, 'Unknown error');
console.warn(`[AppConfig] Attempt ${attempt + 1} failed (status ${status || 'network error'}):`, message, '- will retry...');
continue;
}
// Final attempt failed or non-retryable error (4xx)
const errorMessage = err?.response?.data?.message || err?.message || 'Unknown error occurred';
const errorMessage = extractErrorMessage(error, 'Unknown error occurred');
setError(errorMessage);
console.error(`[AppConfig] Failed to fetch app config after ${attempt + 1} attempts:`, err);
console.error(`[AppConfig] Failed to fetch app config after ${attempt + 1} attempts:`, error);
break;
}
}

View File

@@ -1,10 +1,29 @@
import { useMemo, useState, useEffect } from 'react';
import axios from 'axios';
import apiClient from '@app/services/apiClient';
interface EndpointConfig {
backendUrl: string;
}
interface ErrorPayload {
message?: string;
error?: string;
}
function extractErrorMessage(error: unknown, fallback: string): string {
if (axios.isAxiosError<ErrorPayload>(error)) {
return error.response?.data?.message
|| error.response?.data?.error
|| error.message
|| fallback;
}
if (error instanceof Error) {
return error.message;
}
return fallback;
}
/**
* Desktop-specific endpoint checker that hits the backend directly via axios.
*/
@@ -34,8 +53,8 @@ export function useEndpointEnabled(endpoint: string): {
});
setEnabled(response.data);
} catch (err: any) {
const message = err?.response?.data?.message || err?.message || 'Unknown error occurred';
} catch (error: unknown) {
const message = extractErrorMessage(error, 'Unknown error occurred');
setError(message);
setEnabled(null);
} finally {
@@ -83,8 +102,8 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
});
setEndpointStatus(response.data);
} catch (err: any) {
const message = err?.response?.data?.message || err?.message || 'Unknown error occurred';
} catch (error: unknown) {
const message = extractErrorMessage(error, 'Unknown error occurred');
setError(message);
const fallbackStatus = endpoints.reduce((acc, endpointName) => {