From 614da8d6a828a1ff547de38f164a0eb2e7ebc38c Mon Sep 17 00:00:00 2001 From: James Brunton Date: Fri, 7 Nov 2025 13:26:39 +0000 Subject: [PATCH] Fix more `any` type uses --- frontend/src/core/components/FileManager.tsx | 4 +- .../providers/PDFAnnotationProvider.tsx | 11 ++- .../annotation/shared/BaseAnnotationTool.tsx | 14 ++- .../annotation/tools/DrawingTool.tsx | 29 +++--- .../components/annotation/tools/ImageTool.tsx | 73 +++++++------- .../components/annotation/tools/TextTool.tsx | 30 +++--- .../core/components/pageEditor/PageEditor.tsx | 17 ++-- .../components/pageEditor/PageThumbnail.tsx | 15 +-- .../pageEditor/commands/pageCommands.ts | 1 - .../src/core/components/shared/Tooltip.tsx | 59 ++++++----- .../configSections/AdminAdvancedSection.tsx | 12 ++- .../AdminConnectionsSection.tsx | 29 +++--- .../configSections/AdminDatabaseSection.tsx | 10 +- .../configSections/AdminFeaturesSection.tsx | 10 +- .../configSections/AdminGeneralSection.tsx | 12 ++- .../configSections/AdminPrivacySection.tsx | 16 +-- .../configSections/AdminSecuritySection.tsx | 12 ++- .../shared/config/configSections/Overview.tsx | 13 ++- .../config/configSections/PeopleSection.tsx | 99 +++++++++++-------- .../config/configSections/ProviderCard.tsx | 28 +++--- .../configSections/TeamDetailsSection.tsx | 68 +++++++------ .../config/configSections/TeamsSection.tsx | 48 +++++---- .../configSections/providerDefinitions.ts | 18 +++- .../components/tools/FullscreenToolList.tsx | 13 +-- .../addAttachments/AddAttachmentsSettings.tsx | 4 +- .../AddPageNumbersAppearanceSettings.tsx | 4 +- .../addPageNumbers/PageNumberPreview.tsx | 2 +- .../StampPositionFormattingSettings.tsx | 12 +-- .../tools/addStamp/StampPreview.tsx | 14 +-- .../tools/addStamp/StampSetupSettings.tsx | 4 +- .../AdjustContrastBasicSettings.tsx | 7 +- .../AdjustContrastColorSettings.tsx | 7 +- .../tools/automate/AutomationCreation.tsx | 4 +- .../tools/automate/AutomationEntry.tsx | 2 +- .../components/tools/automate/ToolList.tsx | 4 +- .../tools/automate/ToolSelector.tsx | 13 ++- .../BookletImpositionSettings.tsx | 4 +- .../certSign/CertificateFilesSettings.tsx | 4 +- .../certSign/CertificateFormatSettings.tsx | 4 +- .../certSign/CertificateTypeSettings.tsx | 2 +- .../certSign/SignatureAppearanceSettings.tsx | 6 +- .../extractImages/ExtractImagesSettings.tsx | 6 +- .../tools/shared/createToolFlow.tsx | 2 +- .../tools/shared/renderToolButtons.tsx | 4 +- .../core/components/viewer/EmbedPdfViewer.tsx | 4 +- .../components/viewer/HistoryAPIBridge.tsx | 27 +++-- .../core/components/viewer/LocalEmbedPDF.tsx | 45 +++++---- .../viewer/LocalEmbedPDFWithAnnotations.tsx | 11 +-- .../components/viewer/SearchAPIBridge.tsx | 9 +- .../components/viewer/SignatureAPIBridge.tsx | 58 +++++++---- .../components/viewer/ThumbnailSidebar.tsx | 2 +- .../src/core/components/viewer/viewerTypes.ts | 13 ++- frontend/src/core/contexts/ViewerContext.tsx | 20 ++-- .../src/core/contexts/file/fileActions.ts | 2 +- frontend/src/core/data/toolsTaxonomy.ts | 4 +- .../core/data/useTranslatedToolRegistry.tsx | 65 ++++++------ .../hooks/tools/automate/useAutomationForm.ts | 8 +- .../useRemoveAnnotationsOperation.ts | 4 +- .../useRemovePasswordOperation.test.ts | 2 +- .../hooks/tools/shared/useToolOperation.ts | 24 +++-- frontend/src/core/hooks/useAdminSettings.ts | 2 +- frontend/src/core/hooks/useFileWithUrl.ts | 8 +- frontend/src/core/hooks/useSuggestedTools.ts | 2 +- frontend/src/core/pages/HomePage.tsx | 4 +- frontend/src/core/services/fileAnalyzer.ts | 2 +- .../src/core/services/pdfProcessingService.ts | 3 +- frontend/src/core/services/teamService.ts | 38 +++---- frontend/src/core/types/parameters.ts | 18 +++- frontend/src/core/types/tool.ts | 7 +- frontend/src/core/utils/convertUtils.test.ts | 12 +-- .../src/core/utils/signatureFlattening.ts | 44 ++++----- frontend/src/index.tsx | 2 +- .../src/proprietary/auth/springAuthClient.ts | 4 +- .../components/shared/DividerWithText.tsx | 4 +- .../proprietary/routes/login/OAuthButtons.tsx | 19 +++- 75 files changed, 700 insertions(+), 517 deletions(-) diff --git a/frontend/src/core/components/FileManager.tsx b/frontend/src/core/components/FileManager.tsx index 440ab4acb..fbdc8a613 100644 --- a/frontend/src/core/components/FileManager.tsx +++ b/frontend/src/core/components/FileManager.tsx @@ -4,7 +4,7 @@ import { Dropzone } from '@mantine/dropzone'; import { StirlingFileStub } from '@app/types/fileContext'; import { useFileManager } from '@app/hooks/useFileManager'; import { useFilesModalContext } from '@app/contexts/FilesModalContext'; -import { Tool } from '@app/types/tool'; +import type { ToolRegistryEntry } from '@app/data/toolsTaxonomy'; import MobileLayout from '@app/components/fileManager/MobileLayout'; import DesktopLayout from '@app/components/fileManager/DesktopLayout'; import DragOverlay from '@app/components/fileManager/DragOverlay'; @@ -14,7 +14,7 @@ import { isGoogleDriveConfigured } from '@app/services/googleDrivePickerService' import { loadScript } from '@app/utils/scriptLoader'; interface FileManagerProps { - selectedTool?: Tool | null; + selectedTool?: ToolRegistryEntry | null; } const FileManager: React.FC = ({ selectedTool }) => { diff --git a/frontend/src/core/components/annotation/providers/PDFAnnotationProvider.tsx b/frontend/src/core/components/annotation/providers/PDFAnnotationProvider.tsx index 0979d59e3..afd2b1512 100644 --- a/frontend/src/core/components/annotation/providers/PDFAnnotationProvider.tsx +++ b/frontend/src/core/components/annotation/providers/PDFAnnotationProvider.tsx @@ -1,4 +1,5 @@ import React, { createContext, useContext, ReactNode } from 'react'; +import type { SignParameters } from '@app/hooks/tools/sign/useSignParameters'; interface PDFAnnotationContextValue { // Drawing mode management @@ -22,8 +23,8 @@ interface PDFAnnotationContextValue { isPlacementMode: boolean; // Signature configuration - signatureConfig: any | null; - setSignatureConfig: (config: any | null) => void; + signatureConfig: SignParameters | null; + setSignatureConfig: (config: SignParameters | null) => void; } const PDFAnnotationContext = createContext(undefined); @@ -41,8 +42,8 @@ interface PDFAnnotationProviderProps { storeImageData: (id: string, data: string) => void; getImageData: (id: string) => string | undefined; isPlacementMode: boolean; - signatureConfig: any | null; - setSignatureConfig: (config: any | null) => void; + signatureConfig: SignParameters | null; + setSignatureConfig: (config: SignParameters | null) => void; } export const PDFAnnotationProvider: React.FC = ({ @@ -88,4 +89,4 @@ export const usePDFAnnotation = (): PDFAnnotationContextValue => { throw new Error('usePDFAnnotation must be used within a PDFAnnotationProvider'); } return context; -}; \ No newline at end of file +}; diff --git a/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx b/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx index c61b61cfd..8c99b3b26 100644 --- a/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx +++ b/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx @@ -13,9 +13,17 @@ export interface AnnotationToolConfig { placeButtonText?: string; } +interface BaseAnnotationToolRenderProps { + selectedColor: string; + signatureData: string | null; + onSignatureDataChange: (data: string | null) => void; + onColorSwatchClick: () => void; + disabled: boolean; +} + interface BaseAnnotationToolProps { config: AnnotationToolConfig; - children: React.ReactNode; + children: (props: BaseAnnotationToolRenderProps) => React.ReactNode; onSignatureDataChange?: (data: string | null) => void; disabled?: boolean; } @@ -62,7 +70,7 @@ export const BaseAnnotationTool: React.FC = ({ /> {/* Tool Content */} - {React.cloneElement(children as React.ReactElement, { + {children({ selectedColor, signatureData, onSignatureDataChange: handleSignatureDataChange, @@ -86,4 +94,4 @@ export const BaseAnnotationTool: React.FC = ({ /> ); -}; \ No newline at end of file +}; diff --git a/frontend/src/core/components/annotation/tools/DrawingTool.tsx b/frontend/src/core/components/annotation/tools/DrawingTool.tsx index f3643de35..d278d09cb 100644 --- a/frontend/src/core/components/annotation/tools/DrawingTool.tsx +++ b/frontend/src/core/components/annotation/tools/DrawingTool.tsx @@ -12,7 +12,6 @@ export const DrawingTool: React.FC = ({ onDrawingChange, disabled = false }) => { - const [selectedColor] = useState('#000000'); const [penSize, setPenSize] = useState(2); const [penSizeInput, setPenSizeInput] = useState('2'); @@ -28,18 +27,20 @@ export const DrawingTool: React.FC = ({ onSignatureDataChange={onDrawingChange} disabled={disabled} > - - {}} // Color picker handled by BaseAnnotationTool - onPenSizeChange={setPenSize} - onPenSizeInputChange={setPenSizeInput} - onSignatureDataChange={onDrawingChange || (() => {})} - disabled={disabled} - /> - + {({ selectedColor, onColorSwatchClick, onSignatureDataChange }) => ( + + + + )} ); -}; \ No newline at end of file +}; diff --git a/frontend/src/core/components/annotation/tools/ImageTool.tsx b/frontend/src/core/components/annotation/tools/ImageTool.tsx index d9b4defec..673bf4006 100644 --- a/frontend/src/core/components/annotation/tools/ImageTool.tsx +++ b/frontend/src/core/components/annotation/tools/ImageTool.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { Stack } from '@mantine/core'; import { BaseAnnotationTool } from '@app/components/annotation/shared/BaseAnnotationTool'; import { ImageUploader } from '@app/components/annotation/shared/ImageUploader'; @@ -12,32 +12,27 @@ export const ImageTool: React.FC = ({ onImageChange, disabled = false }) => { - const [, setImageData] = useState(null); + const readFileAsDataUrl = async (file: File | null): Promise => { + if (!file || disabled) { + return null; + } - const handleImageUpload = async (file: File | null) => { - if (file && !disabled) { - try { - const result = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = (e) => { - if (e.target?.result) { - resolve(e.target.result as string); - } else { - reject(new Error('Failed to read file')); - } - }; - reader.onerror = () => reject(reader.error); - reader.readAsDataURL(file); - }); - - setImageData(result); - onImageChange?.(result); - } catch (error) { - console.error('Error reading file:', error); - } - } else if (!file) { - setImageData(null); - onImageChange?.(null); + try { + return await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target?.result) { + resolve(e.target.result as string); + } else { + reject(new Error('Failed to read file')); + } + }; + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(file); + }); + } catch (error) { + console.error('Error reading file:', error); + return null; } }; @@ -53,15 +48,21 @@ export const ImageTool: React.FC = ({ onSignatureDataChange={onImageChange} disabled={disabled} > - - - + {({ onSignatureDataChange }) => ( + + { + const data = await readFileAsDataUrl(file); + onSignatureDataChange(data); + onImageChange?.(data); + }} + disabled={disabled} + label="Upload Image" + placeholder="Select image file" + hint="Upload a PNG, JPG, or other image file to place on the PDF" + /> + + )} ); -}; \ No newline at end of file +}; diff --git a/frontend/src/core/components/annotation/tools/TextTool.tsx b/frontend/src/core/components/annotation/tools/TextTool.tsx index 471198d8e..d35f2ffad 100644 --- a/frontend/src/core/components/annotation/tools/TextTool.tsx +++ b/frontend/src/core/components/annotation/tools/TextTool.tsx @@ -39,19 +39,21 @@ export const TextTool: React.FC = ({ onSignatureDataChange={handleSignatureDataChange} disabled={disabled} > - - - + {() => ( + + + + )} ); -}; \ No newline at end of file +}; diff --git a/frontend/src/core/components/pageEditor/PageEditor.tsx b/frontend/src/core/components/pageEditor/PageEditor.tsx index 079ea747e..3130b7150 100644 --- a/frontend/src/core/components/pageEditor/PageEditor.tsx +++ b/frontend/src/core/components/pageEditor/PageEditor.tsx @@ -21,7 +21,8 @@ import { SplitCommand, BulkRotateCommand, PageBreakCommand, - UndoManager + UndoManager, + DOMCommand } from '@app/components/pageEditor/commands/pageCommands'; import { GRID_CONSTANTS } from '@app/components/pageEditor/constants'; import { usePageDocument } from '@app/components/pageEditor/hooks/usePageDocument'; @@ -29,6 +30,10 @@ import { usePageEditorState } from '@app/components/pageEditor/hooks/usePageEdit import { parseSelection } from "@app/utils/bulkselection/parseSelection"; import { usePageEditorRightRailButtons } from "@app/components/pageEditor/pageEditorRightRailButtons"; +interface ExecutableCommand { + execute: () => void; +} + export interface PageEditorProps { onFunctionsReady?: (functions: PageEditorFunctions) => void; } @@ -98,7 +103,7 @@ const PageEditor = ({ }, [updateUndoRedoState]); // Wrapper for executeCommand to track unsaved changes - const executeCommandWithTracking = useCallback((command: any) => { + const executeCommandWithTracking = useCallback((command: DOMCommand) => { undoManagerRef.current.executeCommand(command); setHasUnsavedChanges(true); }, [setHasUnsavedChanges]); @@ -213,10 +218,8 @@ const PageEditor = ({ }), [splitPositions, executeCommandWithTracking]); // Command executor for PageThumbnail - const executeCommand = useCallback((command: any) => { - if (command && typeof command.execute === 'function') { - command.execute(); - } + const executeCommand = useCallback((command: ExecutableCommand | null | undefined) => { + command?.execute(); }, []); @@ -789,7 +792,7 @@ const PageEditor = ({ page={page} index={index} totalPages={displayDocument.pages.length} - originalFile={(page as any).originalFileId ? selectors.getFile((page as any).originalFileId) : undefined} + originalFile={page.originalFileId ? selectors.getFile(page.originalFileId) : undefined} selectedPageIds={selectedPageIds} selectionMode={selectionMode} movingPage={movingPage} diff --git a/frontend/src/core/components/pageEditor/PageThumbnail.tsx b/frontend/src/core/components/pageEditor/PageThumbnail.tsx index f0124b2c6..2c14166e5 100644 --- a/frontend/src/core/components/pageEditor/PageThumbnail.tsx +++ b/frontend/src/core/components/pageEditor/PageThumbnail.tsx @@ -15,6 +15,7 @@ import { useFilesModalContext } from '@app/contexts/FilesModalContext'; import styles from '@app/components/pageEditor/PageEditor.module.css'; import HoverActionMenu, { HoverAction } from '@app/components/shared/HoverActionMenu'; +type DragManagedElement = HTMLDivElement & { __dragCleanup?: () => void }; interface PageThumbnailProps { page: PDFPage; @@ -69,7 +70,7 @@ const PageThumbnail: React.FC = ({ const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null); const [isHovered, setIsHovered] = useState(false); const isMobile = useMediaQuery('(max-width: 1024px)'); - const dragElementRef = useRef(null); + const dragElementRef = useRef(null); const [thumbnailUrl, setThumbnailUrl] = useState(page.thumbnail); const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration(); const { openFilesModal } = useFilesModalContext(); @@ -132,8 +133,9 @@ const PageThumbnail: React.FC = ({ const pageElementRef = useCallback((element: HTMLDivElement | null) => { if (element) { - pageRefs.current.set(page.id, element); - dragElementRef.current = element; + const managedElement = element as DragManagedElement; + pageRefs.current.set(page.id, managedElement); + dragElementRef.current = managedElement; const dragCleanup = draggable({ element, @@ -176,14 +178,15 @@ const PageThumbnail: React.FC = ({ onDrop: (_) => {} }); - (element as any).__dragCleanup = () => { + managedElement.__dragCleanup = () => { dragCleanup(); dropCleanup(); }; } else { pageRefs.current.delete(page.id); - if (dragElementRef.current && (dragElementRef.current as any).__dragCleanup) { - (dragElementRef.current as any).__dragCleanup(); + const current = dragElementRef.current; + if (current?.__dragCleanup) { + current.__dragCleanup(); } } }, [page.id, page.pageNumber, pageRefs, selectionMode, selectedPageIds, pdfDocument.pages, onReorderPages]); diff --git a/frontend/src/core/components/pageEditor/commands/pageCommands.ts b/frontend/src/core/components/pageEditor/commands/pageCommands.ts index b39fe663d..be7552bc4 100644 --- a/frontend/src/core/components/pageEditor/commands/pageCommands.ts +++ b/frontend/src/core/components/pageEditor/commands/pageCommands.ts @@ -571,7 +571,6 @@ export class InsertFilesCommand extends DOMCommand { private insertedPages: PDFPage[] = []; private originalDocument: PDFDocument | null = null; private fileDataMap = new Map(); // Store file data for thumbnail generation - private originalProcessedFile: any = null; // Store original ProcessedFile for undo private insertedFileMap = new Map(); // Store inserted files for export constructor( diff --git a/frontend/src/core/components/shared/Tooltip.tsx b/frontend/src/core/components/shared/Tooltip.tsx index 77db24d46..e6f5b57a6 100644 --- a/frontend/src/core/components/shared/Tooltip.tsx +++ b/frontend/src/core/components/shared/Tooltip.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react'; +import React, { useState, useRef, useEffect, useMemo, useCallback, MutableRefObject } from 'react'; import { createPortal } from 'react-dom'; import LocalIcon from '@app/components/shared/LocalIcon'; import { addEventListenerWithCleanup } from '@app/utils/genericUtils'; @@ -10,12 +10,24 @@ import { BASE_PATH } from '@app/constants/app'; import styles from '@app/components/shared/tooltip/Tooltip.module.css'; import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex'; +interface TooltipTriggerProps { + ref?: React.Ref; + onPointerEnter?: (event: React.PointerEvent) => void; + onPointerLeave?: (event: React.PointerEvent) => void; + onMouseDown?: (event: React.MouseEvent) => void; + onMouseUp?: (event: React.MouseEvent) => void; + onClick?: (event: React.MouseEvent) => void; + onFocus?: (event: React.FocusEvent) => void; + onBlur?: (event: React.FocusEvent) => void; + [key: string]: unknown; +} + export interface TooltipProps { sidebarTooltip?: boolean; position?: 'right' | 'left' | 'top' | 'bottom'; content?: React.ReactNode; tips?: TooltipTip[]; - children: React.ReactElement; + children: React.ReactElement; offset?: number; maxWidth?: number | string; minWidth?: number | string; @@ -76,6 +88,7 @@ export const Tooltip: React.FC = ({ const isControlled = controlledOpen !== undefined; const open = (isControlled ? !!controlledOpen : internalOpen) && !disabled; + const childProps = children.props; const setOpen = useCallback( (newOpen: boolean) => { @@ -162,9 +175,9 @@ export const Tooltip: React.FC = ({ const handlePointerEnter = useCallback( (e: React.PointerEvent) => { if (!isPinned && !disabled) openWithDelay(); - (children.props as any)?.onPointerEnter?.(e); + childProps.onPointerEnter?.(e); }, - [isPinned, openWithDelay, children.props, disabled] + [isPinned, openWithDelay, childProps, disabled] ); const handlePointerLeave = useCallback( @@ -173,38 +186,38 @@ export const Tooltip: React.FC = ({ // Moving into the tooltip → keep open if (related && tooltipRef.current && tooltipRef.current.contains(related)) { - (children.props as any)?.onPointerLeave?.(e); + childProps.onPointerLeave?.(e); return; } // Ignore transient leave between mousedown and click if (clickPendingRef.current) { - (children.props as any)?.onPointerLeave?.(e); + childProps.onPointerLeave?.(e); return; } clearTimers(); if (!isPinned) setOpen(false); - (children.props as any)?.onPointerLeave?.(e); + childProps.onPointerLeave?.(e); }, - [clearTimers, isPinned, setOpen, children.props] + [clearTimers, isPinned, setOpen, childProps] ); const handleMouseDown = useCallback( (e: React.MouseEvent) => { clickPendingRef.current = true; - (children.props as any)?.onMouseDown?.(e); + childProps.onMouseDown?.(e); }, - [children.props] + [childProps] ); const handleMouseUp = useCallback( (e: React.MouseEvent) => { // allow microtask turn so click can see this false queueMicrotask(() => (clickPendingRef.current = false)); - (children.props as any)?.onMouseUp?.(e); + childProps.onMouseUp?.(e); }, - [children.props] + [childProps] ); const handleClick = useCallback( @@ -219,31 +232,31 @@ export const Tooltip: React.FC = ({ return; } clickPendingRef.current = false; - (children.props as any)?.onClick?.(e); + childProps.onClick?.(e); }, - [clearTimers, pinOnClick, open, setOpen, children.props] + [clearTimers, pinOnClick, open, setOpen, childProps] ); // Keyboard / focus accessibility const handleFocus = useCallback( (e: React.FocusEvent) => { if (!isPinned && !disabled && openOnFocus) openWithDelay(); - (children.props as any)?.onFocus?.(e); + childProps.onFocus?.(e); }, - [isPinned, openWithDelay, children.props, disabled, openOnFocus] + [isPinned, openWithDelay, childProps, disabled, openOnFocus] ); const handleBlur = useCallback( (e: React.FocusEvent) => { const related = e.relatedTarget as Node | null; if (related && tooltipRef.current && tooltipRef.current.contains(related)) { - (children.props as any)?.onBlur?.(e); + childProps.onBlur?.(e); return; } if (!isPinned) setOpen(false); - (children.props as any)?.onBlur?.(e); + childProps.onBlur?.(e); }, - [isPinned, setOpen, children.props] + [isPinned, setOpen, childProps] ); const handleKeyDown = useCallback((e: React.KeyboardEvent) => { @@ -265,12 +278,14 @@ export const Tooltip: React.FC = ({ ); // Enhance child with handlers and ref - const childWithHandlers = React.cloneElement(children as any, { + const childWithHandlers = React.cloneElement(children, { ref: (node: HTMLElement | null) => { triggerRef.current = node || null; - const originalRef = (children as any).ref; + const originalRef = (children as React.ReactElement & { ref?: React.Ref }).ref; if (typeof originalRef === 'function') originalRef(node); - else if (originalRef && typeof originalRef === 'object') (originalRef as any).current = node; + else if (originalRef && typeof originalRef === 'object') { + (originalRef as MutableRefObject).current = node; + } }, 'aria-describedby': open ? tooltipIdRef.current : undefined, onPointerEnter: handlePointerEnter, diff --git a/frontend/src/core/components/shared/config/configSections/AdminAdvancedSection.tsx b/frontend/src/core/components/shared/config/configSections/AdminAdvancedSection.tsx index b979a22b1..eb44f9a4d 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminAdvancedSection.tsx +++ b/frontend/src/core/components/shared/config/configSections/AdminAdvancedSection.tsx @@ -4,11 +4,12 @@ import { NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group, Accordi import { alert } from '@app/components/toast'; import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal'; import { useRestartServer } from '@app/components/shared/config/useRestartServer'; -import { useAdminSettings } from '@app/hooks/useAdminSettings'; +import { useAdminSettings, type SettingsRecord } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; import apiClient from '@app/services/apiClient'; +import type { SettingsWithPending } from '@app/utils/settingsPendingHelper'; -interface AdvancedSettingsData { +interface AdvancedSettingsData extends Record { enableAlphaFunctionality?: boolean; maxDPI?: number; enableUrlToPDF?: boolean; @@ -74,8 +75,9 @@ export default function AdminAdvancedSection() { const systemData = systemResponse.data || {}; const processExecutorData = processExecutorResponse.data || {}; + type AdvancedSettingsResponse = SettingsWithPending & AdvancedSettingsData; - const result: any = { + const result: AdvancedSettingsResponse = { enableAlphaFunctionality: systemData.enableAlphaFunctionality || false, maxDPI: systemData.maxDPI || 0, enableUrlToPDF: systemData.enableUrlToPDF || false, @@ -95,7 +97,7 @@ export default function AdminAdvancedSection() { }; // Merge pending blocks from both endpoints - const pendingBlock: any = {}; + const pendingBlock: Partial = {}; if (systemData._pending?.enableAlphaFunctionality !== undefined) { pendingBlock.enableAlphaFunctionality = systemData._pending.enableAlphaFunctionality; } @@ -125,7 +127,7 @@ export default function AdminAdvancedSection() { return result; }, saveTransformer: (settings) => { - const deltaSettings: Record = { + const deltaSettings: SettingsRecord = { 'system.enableAlphaFunctionality': settings.enableAlphaFunctionality, 'system.maxDPI': settings.maxDPI, 'system.enableUrlToPDF': settings.enableUrlToPDF, diff --git a/frontend/src/core/components/shared/config/configSections/AdminConnectionsSection.tsx b/frontend/src/core/components/shared/config/configSections/AdminConnectionsSection.tsx index 663295b38..9846658c5 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminConnectionsSection.tsx +++ b/frontend/src/core/components/shared/config/configSections/AdminConnectionsSection.tsx @@ -4,7 +4,7 @@ import { Stack, Text, Loader, Group, Divider, Paper, Switch, Badge } from '@mant import { alert } from '@app/components/toast'; import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal'; import { useRestartServer } from '@app/components/shared/config/useRestartServer'; -import { useAdminSettings } from '@app/hooks/useAdminSettings'; +import { useAdminSettings, type SettingsRecord } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; import ProviderCard from '@app/components/shared/config/configSections/ProviderCard'; import { @@ -12,8 +12,11 @@ import { Provider, } from '@app/components/shared/config/configSections/providerDefinitions'; import apiClient from '@app/services/apiClient'; +import type { SettingsWithPending } from '@app/utils/settingsPendingHelper'; -interface ConnectionsSettingsData { +type ProviderSettingsMap = Record; + +interface ConnectionsSettingsData extends Record { oauth2?: { enabled?: boolean; issuer?: string; @@ -24,13 +27,9 @@ interface ConnectionsSettingsData { blockRegistration?: boolean; useAsUsername?: string; scopes?: string; - client?: { - [key: string]: any; - }; - }; - saml2?: { - [key: string]: any; + client?: Record; }; + saml2?: ProviderSettingsMap; mail?: { enabled?: boolean; enableInvites?: boolean; @@ -68,7 +67,9 @@ export default function AdminConnectionsSection() { const premiumResponse = await apiClient.get('/api/v1/admin/settings/section/premium'); const premiumData = premiumResponse.data || {}; - const result: any = { + type ConnectionsResponse = SettingsWithPending & ConnectionsSettingsData; + + const result: ConnectionsResponse = { oauth2: securityData.oauth2 || {}, saml2: securityData.saml2 || {}, mail: mailData || {}, @@ -76,7 +77,7 @@ export default function AdminConnectionsSection() { }; // Merge pending blocks from all three endpoints - const pendingBlock: any = {}; + const pendingBlock: Partial = {}; if (securityData._pending?.oauth2) { pendingBlock.oauth2 = securityData._pending.oauth2; } @@ -128,7 +129,7 @@ export default function AdminConnectionsSection() { return !!(providerSettings?.clientId); }; - const getProviderSettings = (provider: Provider): Record => { + const getProviderSettings = (provider: Provider): ProviderSettingsMap => { if (provider.id === 'saml2') { return settings.saml2 || {}; } @@ -156,7 +157,7 @@ export default function AdminConnectionsSection() { return settings.oauth2?.client?.[provider.id] || {}; }; - const handleProviderSave = async (provider: Provider, providerSettings: Record) => { + const handleProviderSave = async (provider: Provider, providerSettings: ProviderSettingsMap) => { try { if (provider.id === 'smtp') { // Mail settings use a different endpoint @@ -175,7 +176,7 @@ export default function AdminConnectionsSection() { } } else { // OAuth2/SAML2 use delta settings - const deltaSettings: Record = {}; + const deltaSettings: SettingsRecord = {}; if (provider.id === 'saml2') { // SAML2 settings @@ -235,7 +236,7 @@ export default function AdminConnectionsSection() { throw new Error('Failed to disconnect'); } } else { - const deltaSettings: Record = {}; + const deltaSettings: SettingsRecord = {}; if (provider.id === 'saml2') { deltaSettings['security.saml2.enabled'] = false; diff --git a/frontend/src/core/components/shared/config/configSections/AdminDatabaseSection.tsx b/frontend/src/core/components/shared/config/configSections/AdminDatabaseSection.tsx index 647364f19..5e988d726 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminDatabaseSection.tsx +++ b/frontend/src/core/components/shared/config/configSections/AdminDatabaseSection.tsx @@ -4,11 +4,12 @@ import { NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group, TextInp import { alert } from '@app/components/toast'; import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal'; import { useRestartServer } from '@app/components/shared/config/useRestartServer'; -import { useAdminSettings } from '@app/hooks/useAdminSettings'; +import { useAdminSettings, type SettingsRecord } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; import apiClient from '@app/services/apiClient'; +import type { SettingsWithPending } from '@app/utils/settingsPendingHelper'; -interface DatabaseSettingsData { +interface DatabaseSettingsData extends Record { enableCustomDatabase?: boolean; customDatabaseUrl?: string; username?: string; @@ -50,7 +51,8 @@ export default function AdminDatabaseSection() { }; // Map pending changes from system._pending.datasource to root level - const result: any = { ...datasource }; + type DatabaseSettingsResponse = SettingsWithPending & DatabaseSettingsData; + const result: DatabaseSettingsResponse = { ...datasource }; if (systemData._pending?.datasource) { result._pending = systemData._pending.datasource; } @@ -59,7 +61,7 @@ export default function AdminDatabaseSection() { }, saveTransformer: (settings) => { // Convert flat settings to dot-notation for delta endpoint - const deltaSettings: Record = { + const deltaSettings: SettingsRecord = { 'system.datasource.enableCustomDatabase': settings.enableCustomDatabase, 'system.datasource.customDatabaseUrl': settings.customDatabaseUrl, 'system.datasource.username': settings.username, diff --git a/frontend/src/core/components/shared/config/configSections/AdminFeaturesSection.tsx b/frontend/src/core/components/shared/config/configSections/AdminFeaturesSection.tsx index cb90e502b..915892618 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminFeaturesSection.tsx +++ b/frontend/src/core/components/shared/config/configSections/AdminFeaturesSection.tsx @@ -4,11 +4,12 @@ import { TextInput, NumberInput, Switch, Button, Stack, Paper, Text, Loader, Gro import { alert } from '@app/components/toast'; import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal'; import { useRestartServer } from '@app/components/shared/config/useRestartServer'; -import { useAdminSettings } from '@app/hooks/useAdminSettings'; +import { useAdminSettings, type SettingsRecord } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; import apiClient from '@app/services/apiClient'; +import type { SettingsWithPending } from '@app/utils/settingsPendingHelper'; -interface FeaturesSettingsData { +interface FeaturesSettingsData extends Record { serverCertificate?: { enabled?: boolean; organizationName?: string; @@ -35,7 +36,8 @@ export default function AdminFeaturesSection() { const systemResponse = await apiClient.get('/api/v1/admin/settings/section/system'); const systemData = systemResponse.data || {}; - const result: any = { + type FeaturesSettingsResponse = SettingsWithPending & FeaturesSettingsData; + const result: FeaturesSettingsResponse = { serverCertificate: systemData.serverCertificate || { enabled: true, organizationName: 'Stirling-PDF', @@ -52,7 +54,7 @@ export default function AdminFeaturesSection() { return result; }, saveTransformer: (settings) => { - const deltaSettings: Record = {}; + const deltaSettings: SettingsRecord = {}; if (settings.serverCertificate) { deltaSettings['system.serverCertificate.enabled'] = settings.serverCertificate.enabled; diff --git a/frontend/src/core/components/shared/config/configSections/AdminGeneralSection.tsx b/frontend/src/core/components/shared/config/configSections/AdminGeneralSection.tsx index 249dc62b2..6b8c4cd38 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminGeneralSection.tsx +++ b/frontend/src/core/components/shared/config/configSections/AdminGeneralSection.tsx @@ -4,11 +4,12 @@ import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, MultiSele import { alert } from '@app/components/toast'; import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal'; import { useRestartServer } from '@app/components/shared/config/useRestartServer'; -import { useAdminSettings } from '@app/hooks/useAdminSettings'; +import { useAdminSettings, type SettingsRecord } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; import apiClient from '@app/services/apiClient'; +import type { SettingsWithPending } from '@app/utils/settingsPendingHelper'; -interface GeneralSettingsData { +interface GeneralSettingsData extends Record { ui: { appNameNavbar?: string; languages?: string[]; @@ -62,8 +63,9 @@ export default function AdminGeneralSection() { const ui = uiResponse.data || {}; const system = systemResponse.data || {}; const premium = premiumResponse.data || {}; + type GeneralSettingsResponse = SettingsWithPending & GeneralSettingsData; - const result: any = { + const result: GeneralSettingsResponse = { ui, system, customPaths: system.customPaths || { @@ -85,7 +87,7 @@ export default function AdminGeneralSection() { }; // Merge pending blocks from all three endpoints - const pendingBlock: any = {}; + const pendingBlock: Partial = {}; if (ui._pending) { pendingBlock.ui = ui._pending; } @@ -106,7 +108,7 @@ export default function AdminGeneralSection() { return result; }, saveTransformer: (settings) => { - const deltaSettings: Record = { + const deltaSettings: SettingsRecord = { // UI settings 'ui.appNameNavbar': settings.ui.appNameNavbar, 'ui.languages': settings.ui.languages, diff --git a/frontend/src/core/components/shared/config/configSections/AdminPrivacySection.tsx b/frontend/src/core/components/shared/config/configSections/AdminPrivacySection.tsx index efe1a46f1..1eb18ec4f 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminPrivacySection.tsx +++ b/frontend/src/core/components/shared/config/configSections/AdminPrivacySection.tsx @@ -4,11 +4,12 @@ import { Switch, Button, Stack, Paper, Text, Loader, Group } from '@mantine/core import { alert } from '@app/components/toast'; import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal'; import { useRestartServer } from '@app/components/shared/config/useRestartServer'; -import { useAdminSettings } from '@app/hooks/useAdminSettings'; +import { useAdminSettings, type SettingsRecord } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; import apiClient from '@app/services/apiClient'; +import type { SettingsWithPending } from '@app/utils/settingsPendingHelper'; -interface PrivacySettingsData { +interface PrivacySettingsData extends Record { enableAnalytics?: boolean; googleVisibility?: boolean; metricsEnabled?: boolean; @@ -34,17 +35,18 @@ export default function AdminPrivacySection() { apiClient.get('/api/v1/admin/settings/section/system') ]); - const metrics = metricsResponse.data; - const system = systemResponse.data; + const metrics = metricsResponse.data || {}; + const system = systemResponse.data || {}; + type PrivacySettingsResponse = SettingsWithPending & PrivacySettingsData; - const result: any = { + const result: PrivacySettingsResponse = { enableAnalytics: system.enableAnalytics || false, googleVisibility: system.googlevisibility || false, metricsEnabled: metrics.enabled || false }; // Merge pending blocks from both endpoints - const pendingBlock: any = {}; + const pendingBlock: Partial = {}; if (system._pending?.enableAnalytics !== undefined) { pendingBlock.enableAnalytics = system._pending.enableAnalytics; } @@ -62,7 +64,7 @@ export default function AdminPrivacySection() { return result; }, saveTransformer: (settings) => { - const deltaSettings = { + const deltaSettings: SettingsRecord = { 'system.enableAnalytics': settings.enableAnalytics, 'system.googlevisibility': settings.googleVisibility, 'metrics.enabled': settings.metricsEnabled diff --git a/frontend/src/core/components/shared/config/configSections/AdminSecuritySection.tsx b/frontend/src/core/components/shared/config/configSections/AdminSecuritySection.tsx index 9d51ecc99..7feebfd64 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminSecuritySection.tsx +++ b/frontend/src/core/components/shared/config/configSections/AdminSecuritySection.tsx @@ -5,11 +5,12 @@ import { alert } from '@app/components/toast'; import LocalIcon from '@app/components/shared/LocalIcon'; import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal'; import { useRestartServer } from '@app/components/shared/config/useRestartServer'; -import { useAdminSettings } from '@app/hooks/useAdminSettings'; +import { useAdminSettings, type SettingsRecord } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; import apiClient from '@app/services/apiClient'; +import type { SettingsWithPending } from '@app/utils/settingsPendingHelper'; -interface SecuritySettingsData { +interface SecuritySettingsData extends Record { enableLogin?: boolean; csrfDisabled?: boolean; loginMethod?: string; @@ -71,7 +72,8 @@ export default function AdminSecuritySection() { const { _pending: premiumPending, ...premiumActive } = premiumData; const { _pending: systemPending, ...systemActive } = systemData; - const combined: any = { + type SecurityResponse = SettingsWithPending & SecuritySettingsData; + const combined: SecurityResponse = { ...securityActive, audit: premiumActive.enterpriseFeatures?.audit || { enabled: false, @@ -94,7 +96,7 @@ export default function AdminSecuritySection() { }; // Merge all _pending blocks - const mergedPending: any = {}; + const mergedPending: Partial = {}; if (securityPending) { Object.assign(mergedPending, securityPending); } @@ -114,7 +116,7 @@ export default function AdminSecuritySection() { saveTransformer: (settings) => { const { audit, html, ...securitySettings } = settings; - const deltaSettings: Record = { + const deltaSettings: SettingsRecord = { 'premium.enterpriseFeatures.audit.enabled': audit?.enabled, 'premium.enterpriseFeatures.audit.level': audit?.level, 'premium.enterpriseFeatures.audit.retentionDays': audit?.retentionDays diff --git a/frontend/src/core/components/shared/config/configSections/Overview.tsx b/frontend/src/core/components/shared/config/configSections/Overview.tsx index 4e552cec0..6089585d3 100644 --- a/frontend/src/core/components/shared/config/configSections/Overview.tsx +++ b/frontend/src/core/components/shared/config/configSections/Overview.tsx @@ -3,11 +3,18 @@ import { Stack, Text, Code, Group, Badge, Alert, Loader } from '@mantine/core'; import { useAppConfig } from '@app/contexts/AppConfigContext'; import { OverviewHeader } from '@app/components/shared/config/OverviewHeader'; +type ConfigDisplayValue = string | number | boolean | Record | null | undefined; +type ConfigSectionData = Record; + +const isConfigObject = (value: ConfigDisplayValue): value is Record => { + return value !== null && typeof value === 'object'; +}; + const Overview: React.FC = () => { const { config, loading, error } = useAppConfig(); - const renderConfigSection = (title: string, data: any) => { - if (!data || typeof data !== 'object') return null; + const renderConfigSection = (title: string, data: ConfigSectionData | null) => { + if (!data) return null; return ( @@ -22,7 +29,7 @@ const Overview: React.FC = () => { {value ? 'true' : 'false'} - ) : typeof value === 'object' ? ( + ) : isConfigObject(value) ? ( {JSON.stringify(value, null, 2)} ) : ( String(value) || 'null' diff --git a/frontend/src/core/components/shared/config/configSections/PeopleSection.tsx b/frontend/src/core/components/shared/config/configSections/PeopleSection.tsx index 3eb90db6e..98ddfbfe8 100644 --- a/frontend/src/core/components/shared/config/configSections/PeopleSection.tsx +++ b/frontend/src/core/components/shared/config/configSections/PeopleSection.tsx @@ -19,6 +19,8 @@ import { SegmentedControl, Tooltip, CloseButton, + type ComboboxItem, + type ComboboxLikeRenderOptionInput, } from '@mantine/core'; import LocalIcon from '@app/components/shared/LocalIcon'; import { alert } from '@app/components/toast'; @@ -50,17 +52,41 @@ export default function PeopleSection() { }); // Form state for email invite - const [emailInviteForm, setEmailInviteForm] = useState({ - emails: '', - role: 'ROLE_USER', - teamId: undefined as number | undefined, - }); +const [emailInviteForm, setEmailInviteForm] = useState({ + emails: '', + role: 'ROLE_USER', + teamId: undefined as number | undefined, +}); // Form state for edit user modal - const [editForm, setEditForm] = useState({ - role: 'ROLE_USER', - teamId: undefined as number | undefined, - }); +const [editForm, setEditForm] = useState({ + role: 'ROLE_USER', + teamId: undefined as number | undefined, +}); + +type ApiErrorResponse = { + response?: { + data?: { + message?: string; + error?: string; + }; + }; + message?: string; +}; + +const extractErrorMessage = (error: unknown, fallback: string): string => { + if (typeof error === 'object' && error !== null) { + const apiError = error as ApiErrorResponse; + return apiError.response?.data?.message + ?? apiError.response?.data?.error + ?? apiError.message + ?? fallback; + } + if (error instanceof Error) { + return error.message; + } + return fallback; +}; useEffect(() => { fetchData(); @@ -123,12 +149,9 @@ export default function PeopleSection() { forceChange: false, }); fetchData(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to create user:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.people.addMember.error'); + const errorMessage = extractErrorMessage(error, t('workspace.people.addMember.error')); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); @@ -177,12 +200,9 @@ export default function PeopleSection() { body: response.errors || response.error }); } - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to invite users:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.people.emailInvite.error', 'Failed to send invites'); + const errorMessage = extractErrorMessage(error, t('workspace.people.emailInvite.error', 'Failed to send invites')); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); @@ -202,12 +222,9 @@ export default function PeopleSection() { alert({ alertType: 'success', title: t('workspace.people.editMember.success') }); closeEditModal(); fetchData(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to update user:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.people.editMember.error'); + const errorMessage = extractErrorMessage(error, t('workspace.people.editMember.error')); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); @@ -219,12 +236,9 @@ export default function PeopleSection() { await userManagementService.toggleUserEnabled(user.username, !user.enabled); alert({ alertType: 'success', title: t('workspace.people.toggleEnabled.success') }); fetchData(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to toggle user status:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.people.toggleEnabled.error'); + const errorMessage = extractErrorMessage(error, t('workspace.people.toggleEnabled.error')); alert({ alertType: 'error', title: errorMessage }); } }; @@ -239,12 +253,9 @@ export default function PeopleSection() { await userManagementService.deleteUser(user.username); alert({ alertType: 'success', title: t('workspace.people.deleteUserSuccess', 'User deleted successfully') }); fetchData(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to delete user:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.people.deleteUserError', 'Failed to delete user'); + const errorMessage = extractErrorMessage(error, t('workspace.people.deleteUserError', 'Failed to delete user')); alert({ alertType: 'error', title: errorMessage }); } }; @@ -271,7 +282,12 @@ export default function PeopleSection() { user.username.toLowerCase().includes(searchQuery.toLowerCase()) ); - const roleOptions = [ + type RoleOptionItem = ComboboxItem & { + description: string; + icon: string; + }; + + const roleOptions: RoleOptionItem[] = [ { value: 'ROLE_ADMIN', label: t('workspace.people.admin'), @@ -286,17 +302,20 @@ export default function PeopleSection() { }, ]; - const renderRoleOption = ({ option }: { option: any }) => ( + const renderRoleOption = ({ option }: ComboboxLikeRenderOptionInput) => { + const roleOption = option as RoleOptionItem; + return ( - +
- {option.label} + {roleOption.label} - {option.description} + {roleOption.description}
- ); + ); + }; const teamOptions = teams.map((team) => ({ value: team.id.toString(), diff --git a/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx b/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx index 6209fb4b6..dd7b7a783 100644 --- a/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx +++ b/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx @@ -4,11 +4,13 @@ import { useTranslation } from 'react-i18next'; import LocalIcon from '@app/components/shared/LocalIcon'; import { Provider, ProviderField } from '@app/components/shared/config/configSections/providerDefinitions'; +type ProviderSettings = Record; + interface ProviderCardProps { provider: Provider; isConfigured: boolean; - settings?: Record; - onSave?: (settings: Record) => void; + settings?: ProviderSettings; + onSave?: (settings: ProviderSettings) => void; onDisconnect?: () => void; } @@ -21,13 +23,13 @@ export default function ProviderCard({ }: ProviderCardProps) { const { t } = useTranslation(); const [expanded, setExpanded] = useState(false); - const [localSettings, setLocalSettings] = useState>(settings); + const [localSettings, setLocalSettings] = useState(settings); // Initialize local settings with defaults when opening an unconfigured provider const handleConnectToggle = () => { if (!isConfigured && !expanded) { // First time opening an unconfigured provider - initialize with defaults - const defaultSettings: Record = {}; + const defaultSettings: ProviderSettings = {}; provider.fields.forEach((field) => { if (field.defaultValue !== undefined) { defaultSettings[field.key] = field.defaultValue; @@ -38,7 +40,7 @@ export default function ProviderCard({ setExpanded(!expanded); }; - const handleFieldChange = (key: string, value: any) => { + const handleFieldChange = (key: string, value: string | number | boolean) => { setLocalSettings((prev) => ({ ...prev, [key]: value })); }; @@ -50,7 +52,7 @@ export default function ProviderCard({ }; const renderField = (field: ProviderField) => { - const value = localSettings[field.key] ?? field.defaultValue ?? ''; + const value = localSettings[field.key] ?? field.defaultValue; switch (field.type) { case 'switch': @@ -61,7 +63,7 @@ export default function ProviderCard({ {field.description} handleFieldChange(field.key, e.target.checked)} /> @@ -74,8 +76,8 @@ export default function ProviderCard({ label={field.label} description={field.description} placeholder={field.placeholder} - value={value} - onChange={(e) => handleFieldChange(field.key, e.target.value)} + value={typeof value === 'string' ? value : value !== undefined ? String(value) : ''} + onChange={(e) => handleFieldChange(field.key, e.currentTarget.value)} /> ); @@ -86,8 +88,8 @@ export default function ProviderCard({ label={field.label} description={field.description} placeholder={field.placeholder} - value={value} - onChange={(e) => handleFieldChange(field.key, e.target.value)} + value={typeof value === 'string' ? value : value !== undefined ? String(value) : ''} + onChange={(e) => handleFieldChange(field.key, e.currentTarget.value)} /> ); @@ -98,8 +100,8 @@ export default function ProviderCard({ label={field.label} description={field.description} placeholder={field.placeholder} - value={value} - onChange={(e) => handleFieldChange(field.key, e.target.value)} + value={typeof value === 'string' ? value : value !== undefined ? String(value) : ''} + onChange={(e) => handleFieldChange(field.key, e.currentTarget.value)} /> ); } diff --git a/frontend/src/core/components/shared/config/configSections/TeamDetailsSection.tsx b/frontend/src/core/components/shared/config/configSections/TeamDetailsSection.tsx index 61dfe2a84..a9f161a82 100644 --- a/frontend/src/core/components/shared/config/configSections/TeamDetailsSection.tsx +++ b/frontend/src/core/components/shared/config/configSections/TeamDetailsSection.tsx @@ -18,8 +18,8 @@ import { } from '@mantine/core'; import LocalIcon from '@app/components/shared/LocalIcon'; import { alert } from '@app/components/toast'; -import { teamService, Team } from '@app/services/teamService'; -import { User, userManagementService } from '@app/services/userManagementService'; +import { teamService, Team, TeamMember } from '@app/services/teamService'; +import { userManagementService } from '@app/services/userManagementService'; import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex'; interface TeamDetailsSectionProps { @@ -31,17 +31,41 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio const { t } = useTranslation(); const [loading, setLoading] = useState(true); const [team, setTeam] = useState(null); - const [teamUsers, setTeamUsers] = useState([]); - const [availableUsers, setAvailableUsers] = useState([]); + const [teamUsers, setTeamUsers] = useState([]); + const [availableUsers, setAvailableUsers] = useState([]); const [allTeams, setAllTeams] = useState([]); const [userLastRequest, setUserLastRequest] = useState>({}); const [addMemberModalOpened, setAddMemberModalOpened] = useState(false); const [changeTeamModalOpened, setChangeTeamModalOpened] = useState(false); - const [selectedUser, setSelectedUser] = useState(null); + const [selectedUser, setSelectedUser] = useState(null); const [selectedUserId, setSelectedUserId] = useState(''); const [selectedTeamId, setSelectedTeamId] = useState(''); const [processing, setProcessing] = useState(false); +type ApiErrorResponse = { + response?: { + data?: { + message?: string; + error?: string; + }; + }; + message?: string; +}; + +const extractErrorMessage = (error: unknown, fallback: string): string => { + if (typeof error === 'object' && error !== null) { + const apiError = error as ApiErrorResponse; + return apiError.response?.data?.message + ?? apiError.response?.data?.error + ?? apiError.message + ?? fallback; + } + if (error instanceof Error) { + return error.message; + } + return fallback; +}; + useEffect(() => { fetchTeamDetails(); fetchAllTeams(); @@ -87,19 +111,16 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio setAddMemberModalOpened(false); setSelectedUserId(''); fetchTeamDetails(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to add member:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.teams.addMemberToTeam.error', 'Failed to add user to team'); + const errorMessage = extractErrorMessage(error, t('workspace.teams.addMemberToTeam.error', 'Failed to add user to team')); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); } }; - const handleRemoveMember = async (user: User) => { + const handleRemoveMember = async (user: TeamMember) => { if (!window.confirm(t('workspace.teams.confirmRemove', `Remove ${user.username} from this team?`))) { return; } @@ -117,19 +138,16 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio await teamService.moveUserToTeam(user.username, user.rolesAsString || 'ROLE_USER', defaultTeam.id); alert({ alertType: 'success', title: t('workspace.teams.removeMemberSuccess', 'User removed from team') }); fetchTeamDetails(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to remove member:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.teams.removeMemberError', 'Failed to remove user from team'); + const errorMessage = extractErrorMessage(error, t('workspace.teams.removeMemberError', 'Failed to remove user from team')); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); } }; - const handleDeleteUser = async (user: User) => { + const handleDeleteUser = async (user: TeamMember) => { const confirmMessage = t('workspace.people.confirmDelete', 'Are you sure you want to delete this user? This action cannot be undone.'); if (!window.confirm(`${confirmMessage}\n\nUser: ${user.username}`)) { return; @@ -140,19 +158,16 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio await userManagementService.deleteUser(user.username); alert({ alertType: 'success', title: t('workspace.people.deleteUserSuccess', 'User deleted successfully') }); fetchTeamDetails(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to delete user:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.people.deleteUserError', 'Failed to delete user'); + const errorMessage = extractErrorMessage(error, t('workspace.people.deleteUserError', 'Failed to delete user')); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); } }; - const openChangeTeamModal = (user: User) => { + const openChangeTeamModal = (user: TeamMember) => { setSelectedUser(user); setSelectedTeamId(user.team?.id?.toString() || ''); setChangeTeamModalOpened(true); @@ -172,12 +187,9 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio setSelectedUser(null); setSelectedTeamId(''); fetchTeamDetails(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to change team:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.teams.changeTeam.error', 'Failed to change team'); + const errorMessage = extractErrorMessage(error, t('workspace.teams.changeTeam.error', 'Failed to change team')); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); diff --git a/frontend/src/core/components/shared/config/configSections/TeamsSection.tsx b/frontend/src/core/components/shared/config/configSections/TeamsSection.tsx index e8b3b5b41..85cf07ef2 100644 --- a/frontend/src/core/components/shared/config/configSections/TeamsSection.tsx +++ b/frontend/src/core/components/shared/config/configSections/TeamsSection.tsx @@ -37,8 +37,31 @@ export default function TeamsSection() { // Form states const [newTeamName, setNewTeamName] = useState(''); - const [renameTeamName, setRenameTeamName] = useState(''); - const [selectedUserId, setSelectedUserId] = useState(''); +const [renameTeamName, setRenameTeamName] = useState(''); +const [selectedUserId, setSelectedUserId] = useState(''); +type ApiErrorResponse = { + response?: { + data?: { + message?: string; + error?: string; + }; + }; + message?: string; +}; + +const extractErrorMessage = (error: unknown, fallback: string): string => { + if (typeof error === 'object' && error !== null) { + const apiError = error as ApiErrorResponse; + return apiError.response?.data?.message + ?? apiError.response?.data?.error + ?? apiError.message + ?? fallback; + } + if (error instanceof Error) { + return error.message; + } + return fallback; +}; useEffect(() => { fetchTeams(); @@ -70,12 +93,9 @@ export default function TeamsSection() { setCreateModalOpened(false); setNewTeamName(''); fetchTeams(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to create team:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.teams.createTeam.error'); + const errorMessage = extractErrorMessage(error, t('workspace.teams.createTeam.error')); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); @@ -96,12 +116,9 @@ export default function TeamsSection() { setSelectedTeam(null); setRenameTeamName(''); fetchTeams(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to rename team:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.teams.renameTeam.error'); + const errorMessage = extractErrorMessage(error, t('workspace.teams.renameTeam.error')); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); @@ -122,12 +139,9 @@ export default function TeamsSection() { await teamService.deleteTeam(team.id); alert({ alertType: 'success', title: t('workspace.teams.deleteTeam.success') }); fetchTeams(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to delete team:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.teams.deleteTeam.error'); + const errorMessage = extractErrorMessage(error, t('workspace.teams.deleteTeam.error')); alert({ alertType: 'error', title: errorMessage }); } }; diff --git a/frontend/src/core/components/shared/config/configSections/providerDefinitions.ts b/frontend/src/core/components/shared/config/configSections/providerDefinitions.ts index f51f997c7..4ba882337 100644 --- a/frontend/src/core/components/shared/config/configSections/providerDefinitions.ts +++ b/frontend/src/core/components/shared/config/configSections/providerDefinitions.ts @@ -1,14 +1,26 @@ export type ProviderType = 'oauth2' | 'saml2'; -export interface ProviderField { +interface ProviderFieldBase { key: string; - type: 'text' | 'password' | 'switch' | 'textarea'; label: string; description: string; placeholder?: string; - defaultValue?: any; } +export type TextFieldType = 'text' | 'password' | 'textarea'; + +export interface ProviderInputField extends ProviderFieldBase { + type: TextFieldType; + defaultValue?: string; +} + +export interface ProviderSwitchField extends ProviderFieldBase { + type: 'switch'; + defaultValue?: boolean; +} + +export type ProviderField = ProviderInputField | ProviderSwitchField; + export interface Provider { id: string; name: string; diff --git a/frontend/src/core/components/tools/FullscreenToolList.tsx b/frontend/src/core/components/tools/FullscreenToolList.tsx index 4c01b492b..2bf298293 100644 --- a/frontend/src/core/components/tools/FullscreenToolList.tsx +++ b/frontend/src/core/components/tools/FullscreenToolList.tsx @@ -23,6 +23,8 @@ interface FullscreenToolListProps { onSelect: (id: ToolId) => void; } +type RecommendedItem = { id: ToolId; tool: ToolRegistryEntry }; + const FullscreenToolList = ({ filteredTools, searchQuery, @@ -42,9 +44,9 @@ const FullscreenToolList = ({ const favoriteToolItems = useFavoriteToolItems(favoriteTools, toolRegistry); const quickSection = useMemo(() => sections.find(section => section.key === 'quick'), [sections]); - const recommendedItems = useMemo(() => { - if (!quickSection) return [] as Array<{ id: string, tool: ToolRegistryEntry }>; - const items: Array<{ id: string, tool: ToolRegistryEntry }> = []; + const recommendedItems: RecommendedItem[] = useMemo(() => { + if (!quickSection) return []; + const items: RecommendedItem[] = []; quickSection.subcategories.forEach(sc => sc.tools.forEach(t => items.push(t))); return items; }, [quickSection]); @@ -177,11 +179,11 @@ const FullscreenToolList = ({ {showDescriptions ? (
- {recommendedItems.map((item: any) => renderToolItem(item.id, item.tool))} + {recommendedItems.map((item) => renderToolItem(item.id, item.tool))}
) : (
- {recommendedItems.map((item: any) => renderToolItem(item.id, item.tool))} + {recommendedItems.map((item) => renderToolItem(item.id, item.tool))}
)} @@ -250,4 +252,3 @@ const FullscreenToolList = ({ export default FullscreenToolList; - diff --git a/frontend/src/core/components/tools/addAttachments/AddAttachmentsSettings.tsx b/frontend/src/core/components/tools/addAttachments/AddAttachmentsSettings.tsx index 80078d0d7..9fb208549 100644 --- a/frontend/src/core/components/tools/addAttachments/AddAttachmentsSettings.tsx +++ b/frontend/src/core/components/tools/addAttachments/AddAttachmentsSettings.tsx @@ -78,8 +78,8 @@ const AddAttachmentsSettings = ({ parameters, onParameterChange, disabled = fals fontWeight: 400, lineHeight: 1.2, display: '-webkit-box', - WebkitLineClamp: 2 as any, - WebkitBoxOrient: 'vertical' as any, + WebkitLineClamp: '2', + WebkitBoxOrient: 'vertical', overflow: 'hidden', whiteSpace: 'normal', wordBreak: 'break-word', diff --git a/frontend/src/core/components/tools/addPageNumbers/AddPageNumbersAppearanceSettings.tsx b/frontend/src/core/components/tools/addPageNumbers/AddPageNumbersAppearanceSettings.tsx index 09b0218a8..1abbd5232 100644 --- a/frontend/src/core/components/tools/addPageNumbers/AddPageNumbersAppearanceSettings.tsx +++ b/frontend/src/core/components/tools/addPageNumbers/AddPageNumbersAppearanceSettings.tsx @@ -26,7 +26,7 @@ const AddPageNumbersAppearanceSettings = ({ onParameterChange('fontType', (v as any) || 'Times')} + onChange={(value) => onParameterChange('fontType', (value as AddPageNumbersParameters['fontType'] | null) ?? 'Times')} data={[ { value: 'Times', label: 'Times Roman' }, { value: 'Helvetica', label: 'Helvetica' }, diff --git a/frontend/src/core/components/tools/addPageNumbers/PageNumberPreview.tsx b/frontend/src/core/components/tools/addPageNumbers/PageNumberPreview.tsx index 830703f21..e27511c31 100644 --- a/frontend/src/core/components/tools/addPageNumbers/PageNumberPreview.tsx +++ b/frontend/src/core/components/tools/addPageNumbers/PageNumberPreview.tsx @@ -216,7 +216,7 @@ export default function PageNumberPreview({ parameters, onParameterChange, file, key={idx} type="button" className={`${styles.gridTile} ${selected || hoverTile === idx ? styles.gridTileSelected : ''} ${hoverTile === idx ? styles.gridTileHovered : ''}`} - onClick={() => onParameterChange('position', idx as any)} + onClick={() => onParameterChange('position', idx)} onMouseEnter={() => setHoverTile(idx)} onMouseLeave={() => setHoverTile(null)} style={{ diff --git a/frontend/src/core/components/tools/addStamp/StampPositionFormattingSettings.tsx b/frontend/src/core/components/tools/addStamp/StampPositionFormattingSettings.tsx index 058b38bc4..045febe06 100644 --- a/frontend/src/core/components/tools/addStamp/StampPositionFormattingSettings.tsx +++ b/frontend/src/core/components/tools/addStamp/StampPositionFormattingSettings.tsx @@ -37,8 +37,8 @@ const StampPositionFormattingSettings = ({ parameters, onParameterChange, disabl onClick={() => { onParameterChange('position', idx); // Ensure we're using grid positioning, not custom overrides - onParameterChange('overrideX', -1 as any); - onParameterChange('overrideY', -1 as any); + onParameterChange('overrideX', -1); + onParameterChange('overrideY', -1); }} disabled={disabled} styles={{ @@ -108,7 +108,7 @@ const StampPositionFormattingSettings = ({ parameters, onParameterChange, disabl /> onParameterChange('fontSize', v as number)} + onChange={(value) => onParameterChange('fontSize', value)} min={1} max={400} step={1} @@ -134,7 +134,7 @@ const StampPositionFormattingSettings = ({ parameters, onParameterChange, disabl /> onParameterChange('rotation', v as number)} + onChange={(value) => onParameterChange('rotation', value)} min={-180} max={180} step={1} @@ -159,7 +159,7 @@ const StampPositionFormattingSettings = ({ parameters, onParameterChange, disabl /> onParameterChange('opacity', v as number)} + onChange={(value) => onParameterChange('opacity', value)} min={0} max={100} step={1} @@ -184,7 +184,7 @@ const StampPositionFormattingSettings = ({ parameters, onParameterChange, disabl { - const nextAlphabet = (v as any) || 'roman'; + onChange={(value) => { + const nextAlphabet = (value as AddStampParameters['alphabet'] | null) ?? 'roman'; onParameterChange('alphabet', nextAlphabet); const nextDefault = getDefaultFontSizeForAlphabet(nextAlphabet); onParameterChange('fontSize', nextDefault); diff --git a/frontend/src/core/components/tools/adjustContrast/AdjustContrastBasicSettings.tsx b/frontend/src/core/components/tools/adjustContrast/AdjustContrastBasicSettings.tsx index 061eac659..e6fad9d2b 100644 --- a/frontend/src/core/components/tools/adjustContrast/AdjustContrastBasicSettings.tsx +++ b/frontend/src/core/components/tools/adjustContrast/AdjustContrastBasicSettings.tsx @@ -14,11 +14,10 @@ export default function AdjustContrastBasicSettings({ parameters, onParameterCha return ( - onParameterChange('contrast', v as any)} disabled={disabled} /> - onParameterChange('brightness', v as any)} disabled={disabled} /> - onParameterChange('saturation', v as any)} disabled={disabled} /> + onParameterChange('contrast', value)} disabled={disabled} /> + onParameterChange('brightness', value)} disabled={disabled} /> + onParameterChange('saturation', value)} disabled={disabled} /> ); } - diff --git a/frontend/src/core/components/tools/adjustContrast/AdjustContrastColorSettings.tsx b/frontend/src/core/components/tools/adjustContrast/AdjustContrastColorSettings.tsx index f713ca24b..9f446943d 100644 --- a/frontend/src/core/components/tools/adjustContrast/AdjustContrastColorSettings.tsx +++ b/frontend/src/core/components/tools/adjustContrast/AdjustContrastColorSettings.tsx @@ -14,11 +14,10 @@ export default function AdjustContrastColorSettings({ parameters, onParameterCha return ( - onParameterChange('red', v as any)} disabled={disabled} /> - onParameterChange('green', v as any)} disabled={disabled} /> - onParameterChange('blue', v as any)} disabled={disabled} /> + onParameterChange('red', value)} disabled={disabled} /> + onParameterChange('green', value)} disabled={disabled} /> + onParameterChange('blue', value)} disabled={disabled} /> ); } - diff --git a/frontend/src/core/components/tools/automate/AutomationCreation.tsx b/frontend/src/core/components/tools/automate/AutomationCreation.tsx index 836c14fed..6915d8969 100644 --- a/frontend/src/core/components/tools/automate/AutomationCreation.tsx +++ b/frontend/src/core/components/tools/automate/AutomationCreation.tsx @@ -16,7 +16,7 @@ import { ToolRegistry } from '@app/data/toolsTaxonomy'; import ToolConfigurationModal from '@app/components/tools/automate/ToolConfigurationModal'; import ToolList from '@app/components/tools/automate/ToolList'; import IconSelector from '@app/components/tools/automate/IconSelector'; -import { AutomationConfig, AutomationMode, AutomationTool } from '@app/types/automation'; +import { AutomationConfig, AutomationMode, AutomationTool, AutomationParameters } from '@app/types/automation'; import { useAutomationForm } from '@app/hooks/tools/automate/useAutomationForm'; @@ -56,7 +56,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o setConfigModalOpen(true); }; - const handleToolConfigSave = (parameters: Record) => { + const handleToolConfigSave = (parameters: AutomationParameters) => { if (configuraingToolIndex >= 0) { updateTool(configuraingToolIndex, { configured: true, diff --git a/frontend/src/core/components/tools/automate/AutomationEntry.tsx b/frontend/src/core/components/tools/automate/AutomationEntry.tsx index 7871033b9..4e11de03a 100644 --- a/frontend/src/core/components/tools/automate/AutomationEntry.tsx +++ b/frontend/src/core/components/tools/automate/AutomationEntry.tsx @@ -16,7 +16,7 @@ interface AutomationEntryProps { /** Optional description for tooltip */ description?: string; /** MUI Icon component for the badge */ - badgeIcon?: React.ComponentType; + badgeIcon?: React.ComponentType; /** Array of tool operation names in the workflow */ operations: string[]; /** Click handler */ diff --git a/frontend/src/core/components/tools/automate/ToolList.tsx b/frontend/src/core/components/tools/automate/ToolList.tsx index 7a5c66860..b5c421b6a 100644 --- a/frontend/src/core/components/tools/automate/ToolList.tsx +++ b/frontend/src/core/components/tools/automate/ToolList.tsx @@ -4,7 +4,7 @@ import { Text, Stack, Group, ActionIcon } from "@mantine/core"; import SettingsIcon from "@mui/icons-material/Settings"; import CloseIcon from "@mui/icons-material/Close"; import AddCircleOutline from "@mui/icons-material/AddCircleOutline"; -import { AutomationTool } from "@app/types/automation"; +import { AutomationTool, AutomationParameters } from "@app/types/automation"; import { ToolRegistry } from "@app/data/toolsTaxonomy"; import { ToolId } from "@app/types/toolId"; import ToolSelector from "@app/components/tools/automate/ToolSelector"; @@ -18,7 +18,7 @@ interface ToolListProps { onToolConfigure: (index: number) => void; onToolAdd: () => void; getToolName: (operation: string) => string; - getToolDefaultParameters: (operation: string) => Record; + getToolDefaultParameters: (operation: string) => AutomationParameters; } export default function ToolList({ diff --git a/frontend/src/core/components/tools/automate/ToolSelector.tsx b/frontend/src/core/components/tools/automate/ToolSelector.tsx index 3fc6b0a2a..ef5f00552 100644 --- a/frontend/src/core/components/tools/automate/ToolSelector.tsx +++ b/frontend/src/core/components/tools/automate/ToolSelector.tsx @@ -1,8 +1,8 @@ import { useState, useMemo, useCallback, useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Stack, Text, ScrollArea } from '@mantine/core'; -import { ToolRegistryEntry, ToolRegistry, getToolSupportsAutomate } from '@app/data/toolsTaxonomy'; -import { useToolSections } from '@app/hooks/useToolSections'; +import { ToolRegistryEntry, ToolRegistry, getToolSupportsAutomate, SubcategoryId } from '@app/data/toolsTaxonomy'; +import { useToolSections, type SubcategoryGroup } from '@app/hooks/useToolSections'; import { renderToolButtons } from '@app/components/tools/shared/renderToolButtons'; import ToolSearch from '@app/components/tools/toolPicker/ToolSearch'; import ToolButton from '@app/components/tools/toolPicker/ToolButton'; @@ -67,11 +67,11 @@ export default function ToolSelector({ }, [filteredTools]); // Use the same tool sections logic as the main ToolPicker - const { sections, searchGroups } = useToolSections(transformedFilteredTools as any /* FIX ME */); + const { sections, searchGroups } = useToolSections(transformedFilteredTools); // Determine what to display: search results or organized sections const isSearching = searchTerm.trim().length > 0; - const displayGroups = useMemo(() => { + const displayGroups = useMemo(() => { if (isSearching) { return searchGroups || []; } @@ -80,8 +80,7 @@ export default function ToolSelector({ // If no sections, create a simple group from filtered tools if (baseFilteredTools.length > 0) { return [{ - name: 'All Tools', - subcategoryId: 'all' as any, + subcategoryId: SubcategoryId.GENERAL, tools: baseFilteredTools.map(([key, tool]) => ({ id: key, tool })) }]; } @@ -101,7 +100,7 @@ export default function ToolSelector({ const renderedTools = useMemo(() => displayGroups.map((subcategory) => - renderToolButtons(t, subcategory as any, null, handleToolSelect, !isSearching, true) + renderToolButtons(t, subcategory, null, handleToolSelect, !isSearching, true) ), [displayGroups, handleToolSelect, isSearching, t] ); diff --git a/frontend/src/core/components/tools/bookletImposition/BookletImpositionSettings.tsx b/frontend/src/core/components/tools/bookletImposition/BookletImpositionSettings.tsx index f0c76f04c..847ef08ec 100644 --- a/frontend/src/core/components/tools/bookletImposition/BookletImpositionSettings.tsx +++ b/frontend/src/core/components/tools/bookletImposition/BookletImpositionSettings.tsx @@ -6,7 +6,7 @@ import ButtonSelector from "@app/components/shared/ButtonSelector"; interface BookletImpositionSettingsProps { parameters: BookletImpositionParameters; - onParameterChange: (key: keyof BookletImpositionParameters, value: any) => void; + onParameterChange: (key: K, value: BookletImpositionParameters[K]) => void; disabled?: boolean; } @@ -136,7 +136,7 @@ const BookletImpositionSettings = ({ parameters, onParameterChange, disabled = f onParameterChange('gutterSize', value || 12)} + onChange={(value) => onParameterChange('gutterSize', typeof value === 'number' ? value : parameters.gutterSize)} min={6} max={72} step={6} diff --git a/frontend/src/core/components/tools/certSign/CertificateFilesSettings.tsx b/frontend/src/core/components/tools/certSign/CertificateFilesSettings.tsx index ffedbe70d..e1f3202db 100644 --- a/frontend/src/core/components/tools/certSign/CertificateFilesSettings.tsx +++ b/frontend/src/core/components/tools/certSign/CertificateFilesSettings.tsx @@ -5,7 +5,7 @@ import FileUploadButton from "@app/components/shared/FileUploadButton"; interface CertificateFilesSettingsProps { parameters: CertSignParameters; - onParameterChange: (key: keyof CertSignParameters, value: any) => void; + onParameterChange: (key: K, value: CertSignParameters[K]) => void; disabled?: boolean; } @@ -92,4 +92,4 @@ const CertificateFilesSettings = ({ parameters, onParameterChange, disabled = fa ); }; -export default CertificateFilesSettings; \ No newline at end of file +export default CertificateFilesSettings; diff --git a/frontend/src/core/components/tools/certSign/CertificateFormatSettings.tsx b/frontend/src/core/components/tools/certSign/CertificateFormatSettings.tsx index 6e584618e..5de3b7cb5 100644 --- a/frontend/src/core/components/tools/certSign/CertificateFormatSettings.tsx +++ b/frontend/src/core/components/tools/certSign/CertificateFormatSettings.tsx @@ -3,7 +3,7 @@ import { CertSignParameters } from "@app/hooks/tools/certSign/useCertSignParamet interface CertificateFormatSettingsProps { parameters: CertSignParameters; - onParameterChange: (key: keyof CertSignParameters, value: any) => void; + onParameterChange: (key: K, value: CertSignParameters[K]) => void; disabled?: boolean; } @@ -67,4 +67,4 @@ const CertificateFormatSettings = ({ parameters, onParameterChange, disabled = f ); }; -export default CertificateFormatSettings; \ No newline at end of file +export default CertificateFormatSettings; diff --git a/frontend/src/core/components/tools/certSign/CertificateTypeSettings.tsx b/frontend/src/core/components/tools/certSign/CertificateTypeSettings.tsx index 4cf85c66c..8e5678819 100644 --- a/frontend/src/core/components/tools/certSign/CertificateTypeSettings.tsx +++ b/frontend/src/core/components/tools/certSign/CertificateTypeSettings.tsx @@ -4,7 +4,7 @@ import { useAppConfig } from "@app/contexts/AppConfigContext"; interface CertificateTypeSettingsProps { parameters: CertSignParameters; - onParameterChange: (key: keyof CertSignParameters, value: any) => void; + onParameterChange: (key: K, value: CertSignParameters[K]) => void; disabled?: boolean; } diff --git a/frontend/src/core/components/tools/certSign/SignatureAppearanceSettings.tsx b/frontend/src/core/components/tools/certSign/SignatureAppearanceSettings.tsx index f6ddbe241..303f4be04 100644 --- a/frontend/src/core/components/tools/certSign/SignatureAppearanceSettings.tsx +++ b/frontend/src/core/components/tools/certSign/SignatureAppearanceSettings.tsx @@ -4,7 +4,7 @@ import { CertSignParameters } from "@app/hooks/tools/certSign/useCertSignParamet interface SignatureAppearanceSettingsProps { parameters: CertSignParameters; - onParameterChange: (key: keyof CertSignParameters, value: any) => void; + onParameterChange: (key: K, value: CertSignParameters[K]) => void; disabled?: boolean; } @@ -68,7 +68,7 @@ const SignatureAppearanceSettings = ({ parameters, onParameterChange, disabled = onParameterChange('pageNumber', value || 1)} + onChange={(value) => onParameterChange('pageNumber', typeof value === 'number' ? value : parameters.pageNumber)} min={1} disabled={disabled} /> @@ -107,4 +107,4 @@ const SignatureAppearanceSettings = ({ parameters, onParameterChange, disabled = ); }; -export default SignatureAppearanceSettings; \ No newline at end of file +export default SignatureAppearanceSettings; diff --git a/frontend/src/core/components/tools/extractImages/ExtractImagesSettings.tsx b/frontend/src/core/components/tools/extractImages/ExtractImagesSettings.tsx index 6d4b9dc10..288520ad2 100644 --- a/frontend/src/core/components/tools/extractImages/ExtractImagesSettings.tsx +++ b/frontend/src/core/components/tools/extractImages/ExtractImagesSettings.tsx @@ -22,8 +22,8 @@ const ExtractImagesSettings = ({ value={parameters.format} onChange={(value) => { const allowedFormats = ['png', 'jpg', 'gif'] as const; - const format = allowedFormats.includes(value as any) ? (value as typeof allowedFormats[number]) : 'png'; - onParameterChange('format', format); + const nextFormat = allowedFormats.find((format) => format === value) ?? 'png'; + onParameterChange('format', nextFormat); }} data={[ { value: 'png', label: 'PNG' }, @@ -43,4 +43,4 @@ const ExtractImagesSettings = ({ ); }; -export default ExtractImagesSettings; \ No newline at end of file +export default ExtractImagesSettings; diff --git a/frontend/src/core/components/tools/shared/createToolFlow.tsx b/frontend/src/core/components/tools/shared/createToolFlow.tsx index d3b683777..ae74bdaf3 100644 --- a/frontend/src/core/components/tools/shared/createToolFlow.tsx +++ b/frontend/src/core/components/tools/shared/createToolFlow.tsx @@ -42,7 +42,7 @@ export interface ExecuteButtonConfig { export interface ReviewStepConfig { isVisible: boolean; - operation: ToolOperationHook; + operation: ToolOperationHook; title: string; onFileClick?: (file: File) => void; onUndo: () => void; diff --git a/frontend/src/core/components/tools/shared/renderToolButtons.tsx b/frontend/src/core/components/tools/shared/renderToolButtons.tsx index 8e1b986cf..b5c16f4a6 100644 --- a/frontend/src/core/components/tools/shared/renderToolButtons.tsx +++ b/frontend/src/core/components/tools/shared/renderToolButtons.tsx @@ -2,7 +2,7 @@ import { Box } from '@mantine/core'; import ToolButton from '@app/components/tools/toolPicker/ToolButton'; import SubcategoryHeader from '@app/components/tools/shared/SubcategoryHeader'; -import { getSubcategoryLabel } from "@app/data/toolsTaxonomy"; +import { getSubcategoryLabel, type ToolRegistryEntry } from "@app/data/toolsTaxonomy"; import { TFunction } from 'i18next'; import { SubcategoryGroup } from '@app/hooks/useToolSections'; import { ToolId } from "@app/types/toolId"; @@ -15,7 +15,7 @@ export const renderToolButtons = ( onSelect: (id: ToolId) => void, showSubcategoryHeader: boolean = true, disableNavigation: boolean = false, - searchResults?: Array<{ item: [string, any]; matchedText?: string }>, + searchResults?: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>, hasStars: boolean = false ) => { // Create a map of matched text for quick lookup diff --git a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx index f85058a73..abb3bd5b6 100644 --- a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx +++ b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx @@ -290,8 +290,8 @@ const EmbedPdfViewerContent = ({ file={effectiveFile.file} url={effectiveFile.url} enableAnnotations={shouldEnableAnnotations} - signatureApiRef={signatureApiRef as React.RefObject} - historyApiRef={historyApiRef as React.RefObject} + signatureApiRef={signatureApiRef} + historyApiRef={historyApiRef} onSignatureAdded={() => { // Handle signature added - for debugging, enable console logs as needed // Future: Handle signature completion diff --git a/frontend/src/core/components/viewer/HistoryAPIBridge.tsx b/frontend/src/core/components/viewer/HistoryAPIBridge.tsx index bdbf80429..b880f6282 100644 --- a/frontend/src/core/components/viewer/HistoryAPIBridge.tsx +++ b/frontend/src/core/components/viewer/HistoryAPIBridge.tsx @@ -2,8 +2,10 @@ import { useImperativeHandle, forwardRef, useEffect } from 'react'; import { useHistoryCapability } from '@embedpdf/plugin-history/react'; import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react'; import { useSignature } from '@app/contexts/SignatureContext'; -import { uuidV4 } from '@embedpdf/models'; -import type { HistoryAPI } from '@app/components/viewer/viewerTypes'; +import { uuidV4, type PdfAnnotationObject } from '@embedpdf/models'; +import type { HistoryAPI, AnnotationEvent } from '@app/components/viewer/viewerTypes'; + +type StampAnnotation = PdfAnnotationObject & { imageSrc?: string }; export const HistoryAPIBridge = forwardRef(function HistoryAPIBridge(_, ref) { const { provides: historyApi } = useHistoryCapability(); @@ -14,14 +16,15 @@ export const HistoryAPIBridge = forwardRef(function HistoryAPIBridge useEffect(() => { if (!annotationApi) return; - const handleAnnotationEvent = (event: any) => { + const handleAnnotationEvent = (event: AnnotationEvent) => { const annotation = event.annotation; + const stampAnnotation = annotation as StampAnnotation; // Store image data for all STAMP annotations immediately when created or modified - if (annotation && annotation.type === 13 && annotation.id && annotation.imageSrc) { + if (annotation && annotation.type === 13 && annotation.id && stampAnnotation.imageSrc) { const storedImageData = getImageData(annotation.id); - if (!storedImageData || storedImageData !== annotation.imageSrc) { - storeImageData(annotation.id, annotation.imageSrc); + if (!storedImageData || storedImageData !== stampAnnotation.imageSrc) { + storeImageData(annotation.id, stampAnnotation.imageSrc); } } @@ -29,13 +32,17 @@ export const HistoryAPIBridge = forwardRef(function HistoryAPIBridge if (event.type === 'create' && event.committed) { // Check if this is a STAMP annotation (signature) that might need image data restoration if (annotation && annotation.type === 13 && annotation.id) { + const pageIndex = event.pageIndex; + if (pageIndex === undefined) { + return; + } getImageData(annotation.id); // Delay the check to allow the annotation to be fully created setTimeout(() => { const currentStoredData = getImageData(annotation.id); // Check if the annotation lacks image data but we have it stored - if (currentStoredData && (!annotation.imageSrc || annotation.imageSrc !== currentStoredData)) { + if (currentStoredData && (!stampAnnotation.imageSrc || stampAnnotation.imageSrc !== currentStoredData)) { // Generate new ID to avoid React key conflicts const newId = uuidV4(); @@ -46,7 +53,7 @@ export const HistoryAPIBridge = forwardRef(function HistoryAPIBridge rect: annotation.rect, author: annotation.author || 'Digital Signature', subject: annotation.subject || 'Digital Signature', - pageIndex: event.pageIndex, + pageIndex, id: newId, created: annotation.created || new Date(), imageSrc: currentStoredData @@ -57,10 +64,10 @@ export const HistoryAPIBridge = forwardRef(function HistoryAPIBridge // Replace the annotation with one that has proper image data try { - annotationApi.deleteAnnotation(event.pageIndex, annotation.id); + annotationApi.deleteAnnotation(pageIndex, annotation.id); // Small delay to ensure deletion completes setTimeout(() => { - annotationApi.createAnnotation(event.pageIndex, restoredData); + annotationApi.createAnnotation(pageIndex, restoredData); }, 50); } catch (error) { console.error('HistoryAPI: Failed to restore annotation:', error); diff --git a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx index bfe75df2f..18fa75f8e 100644 --- a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx +++ b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx @@ -22,7 +22,7 @@ import { ExportPluginPackage } from '@embedpdf/plugin-export/react'; // Import annotation plugins import { HistoryPluginPackage } from '@embedpdf/plugin-history/react'; import { AnnotationLayer, AnnotationPluginPackage } from '@embedpdf/plugin-annotation/react'; -import { PdfAnnotationSubtype } from '@embedpdf/models'; +import { PdfAnnotationSubtype, type PdfAnnotationObject } from '@embedpdf/models'; import { CustomSearchLayer } from '@app/components/viewer/CustomSearchLayer'; import { ZoomAPIBridge } from '@app/components/viewer/ZoomAPIBridge'; import ToolLoadingFallback from '@app/components/tools/ToolLoadingFallback'; @@ -36,21 +36,27 @@ import { ThumbnailAPIBridge } from '@app/components/viewer/ThumbnailAPIBridge'; import { RotateAPIBridge } from '@app/components/viewer/RotateAPIBridge'; import { SignatureAPIBridge } from '@app/components/viewer/SignatureAPIBridge'; import { HistoryAPIBridge } from '@app/components/viewer/HistoryAPIBridge'; -import type { SignatureAPI, HistoryAPI } from '@app/components/viewer/viewerTypes'; +import type { SignatureAPI, HistoryAPI, AnnotationEvent } from '@app/components/viewer/viewerTypes'; import { ExportAPIBridge } from '@app/components/viewer/ExportAPIBridge'; interface LocalEmbedPDFProps { file?: File | Blob; url?: string | null; enableAnnotations?: boolean; - onSignatureAdded?: (annotation: any) => void; - signatureApiRef?: React.RefObject; - historyApiRef?: React.RefObject; + onSignatureAdded?: (annotation: PdfAnnotationObject) => void; + signatureApiRef?: React.RefObject; + historyApiRef?: React.RefObject; } +type AnnotationRecord = { + id: string; + pageIndex: number; + rect: PdfAnnotationObject['rect']; +}; + export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) { const [pdfUrl, setPdfUrl] = useState(null); - const [, setAnnotations] = useState>([]); + const [, setAnnotations] = useState([]); // Convert File to URL if needed useEffect(() => { @@ -233,29 +239,34 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur }); // Listen for annotation events to track annotations and notify parent - annotationApi.onAnnotationEvent((event: any) => { - if (event.type === 'create' && event.committed) { + annotationApi.onAnnotationEvent((event: AnnotationEvent) => { + if (event.type === 'create' && event.committed && event.annotation) { + const annotation = event.annotation; + if (!annotation.id) { + return; + } // Add to annotations list setAnnotations(prev => [...prev, { - id: event.annotation.id, - pageIndex: event.pageIndex, - rect: event.annotation.rect + id: annotation.id, + pageIndex: event.pageIndex ?? 0, + rect: annotation.rect }]); // Notify parent if callback provided if (onSignatureAdded) { - onSignatureAdded(event.annotation); + onSignatureAdded(annotation); } - } else if (event.type === 'delete' && event.committed) { + } else if (event.type === 'delete' && event.committed && event.annotation?.id) { // Remove from annotations list - setAnnotations(prev => prev.filter(ann => ann.id !== event.annotation.id)); + const annotationId = event.annotation.id; + setAnnotations(prev => prev.filter(ann => ann.id !== annotationId)); } else if (event.type === 'loaded') { // Handle initial load of annotations const loadedAnnotations = event.annotations || []; - setAnnotations(loadedAnnotations.map((ann: any) => ({ - id: ann.id, - pageIndex: ann.pageIndex || 0, + setAnnotations(loadedAnnotations.map((ann) => ({ + id: ann.id ?? '', + pageIndex: ann.pageIndex ?? event.pageIndex ?? 0, rect: ann.rect }))); } diff --git a/frontend/src/core/components/viewer/LocalEmbedPDFWithAnnotations.tsx b/frontend/src/core/components/viewer/LocalEmbedPDFWithAnnotations.tsx index 87a5d7514..07f214c5c 100644 --- a/frontend/src/core/components/viewer/LocalEmbedPDFWithAnnotations.tsx +++ b/frontend/src/core/components/viewer/LocalEmbedPDFWithAnnotations.tsx @@ -22,7 +22,8 @@ import { Rotation } from '@embedpdf/models'; // Import annotation plugins import { HistoryPluginPackage } from '@embedpdf/plugin-history/react'; import { AnnotationLayer, AnnotationPluginPackage } from '@embedpdf/plugin-annotation/react'; -import { PdfAnnotationSubtype } from '@embedpdf/models'; +import { PdfAnnotationSubtype, type PdfAnnotationObject } from '@embedpdf/models'; +import type { AnnotationEvent } from '@app/components/viewer/viewerTypes'; import { CustomSearchLayer } from '@app/components/viewer/CustomSearchLayer'; import { ZoomAPIBridge } from '@app/components/viewer/ZoomAPIBridge'; @@ -39,7 +40,7 @@ import { RotateAPIBridge } from '@app/components/viewer/RotateAPIBridge'; interface LocalEmbedPDFWithAnnotationsProps { file?: File | Blob; url?: string | null; - onAnnotationChange?: (annotations: any[]) => void; + onAnnotationChange?: (annotations: PdfAnnotationObject[]) => void; } export function LocalEmbedPDFWithAnnotations({ @@ -223,10 +224,8 @@ export function LocalEmbedPDFWithAnnotations({ // Listen for annotation events to notify parent if (onAnnotationChange) { - annotationApi.onAnnotationEvent((event: any) => { - if (event.committed) { - // Get all annotations and notify parent - // This is a simplified approach - in reality you'd need to get all annotations + annotationApi.onAnnotationEvent((event: AnnotationEvent) => { + if (event.committed && event.annotation) { onAnnotationChange([event.annotation]); } }); diff --git a/frontend/src/core/components/viewer/SearchAPIBridge.tsx b/frontend/src/core/components/viewer/SearchAPIBridge.tsx index 4b0eadd23..492084b86 100644 --- a/frontend/src/core/components/viewer/SearchAPIBridge.tsx +++ b/frontend/src/core/components/viewer/SearchAPIBridge.tsx @@ -10,6 +10,11 @@ interface SearchResult { }>; } +interface SearchResultState { + results?: SearchResult[]; + activeResultIndex?: number; +} + /** * SearchAPIBridge manages search state and provides search functionality. * Listens for search result changes from EmbedPDF and maintains local state. @@ -27,7 +32,7 @@ export function SearchAPIBridge() { useEffect(() => { if (!search) return; - const unsubscribe = search.onSearchResultStateChange?.((state: any) => { + const unsubscribe = search.onSearchResultStateChange?.((state: SearchResultState) => { const newState = { results: state?.results || null, activeIndex: (state?.activeResultIndex || 0) + 1 // Convert to 1-based index @@ -53,7 +58,7 @@ export function SearchAPIBridge() { api: { search: async (query: string) => { search.startSearch(); - return search.searchAllPages(query); + await search.searchAllPages(query).toPromise(); }, clear: () => { search.stopSearch(); diff --git a/frontend/src/core/components/viewer/SignatureAPIBridge.tsx b/frontend/src/core/components/viewer/SignatureAPIBridge.tsx index 6eb8d58e0..0a813608d 100644 --- a/frontend/src/core/components/viewer/SignatureAPIBridge.tsx +++ b/frontend/src/core/components/viewer/SignatureAPIBridge.tsx @@ -1,9 +1,20 @@ import { useImperativeHandle, forwardRef, useEffect } from 'react'; import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react'; -import { PdfAnnotationSubtype, uuidV4 } from '@embedpdf/models'; +import { PdfAnnotationSubtype, uuidV4, type PdfAnnotationObject } from '@embedpdf/models'; import { useSignature } from '@app/contexts/SignatureContext'; import type { SignatureAPI } from '@app/components/viewer/viewerTypes'; +type StampAnnotationExtras = { + imageSrc?: string; + imageData?: string; + contents?: string; + data?: string; + appearance?: string; +}; + +type StampAnnotation = PdfAnnotationObject & StampAnnotationExtras; +type SelectedAnnotation = { object?: PdfAnnotationObject }; + export const SignatureAPIBridge = forwardRef(function SignatureAPIBridge(_, ref) { const { provides: annotationApi } = useAnnotationCapability(); const { signatureConfig, storeImageData, isPlacementMode } = useSignature(); @@ -16,21 +27,21 @@ export const SignatureAPIBridge = forwardRef(function SignatureAPI const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Delete' || event.key === 'Backspace') { - const selectedAnnotation = annotationApi.getSelectedAnnotation?.(); + const selectedAnnotation = annotationApi.getSelectedAnnotation?.() as SelectedAnnotation | null; - if (selectedAnnotation) { - const annotation = selectedAnnotation as any; - const pageIndex = annotation.object?.pageIndex || 0; - const id = annotation.object?.id; + if (selectedAnnotation?.object) { + const annotation = selectedAnnotation.object as StampAnnotation; + const pageIndex = annotation.pageIndex || 0; + const id = annotation.id; // For STAMP annotations, ensure image data is preserved before deletion - if (annotation.object?.type === 13 && id) { + if (annotation.type === 13 && id) { // Get current annotation data to ensure we have latest image data stored const pageAnnotationsTask = annotationApi.getPageAnnotations?.({ pageIndex }); if (pageAnnotationsTask) { - pageAnnotationsTask.toPromise().then((pageAnnotations: any) => { - const currentAnn = pageAnnotations?.find((ann: any) => ann.id === id); - if (currentAnn && currentAnn.imageSrc) { + pageAnnotationsTask.toPromise().then((pageAnnotations: PdfAnnotationObject[]) => { + const currentAnn = pageAnnotations?.find((ann) => ann.id === id) as StampAnnotation | undefined; + if (currentAnn?.imageSrc) { // Ensure the image data is stored in our persistent store storeImageData(id, currentAnn.imageSrc); } @@ -39,8 +50,9 @@ export const SignatureAPIBridge = forwardRef(function SignatureAPI } // Use EmbedPDF's native deletion which should integrate with history - if ((annotationApi as any).deleteSelected) { - (annotationApi as any).deleteSelected(); + const deletableApi = annotationApi as typeof annotationApi & { deleteSelected?: () => void }; + if (deletableApi.deleteSelected) { + deletableApi.deleteSelected(); } else { // Fallback to direct deletion - less ideal for history if (id) { @@ -194,13 +206,19 @@ export const SignatureAPIBridge = forwardRef(function SignatureAPI // Before deleting, try to preserve image data for potential undo const pageAnnotationsTask = annotationApi.getPageAnnotations?.({ pageIndex }); if (pageAnnotationsTask) { - pageAnnotationsTask.toPromise().then((pageAnnotations: any) => { - const annotation = pageAnnotations?.find((ann: any) => ann.id === annotationId); - if (annotation && annotation.type === 13 && annotation.imageSrc) { - // Store image data before deletion - storeImageData(annotationId, annotation.imageSrc); - } - }).catch(console.error); + pageAnnotationsTask + .toPromise() + .then((pageAnnotations: PdfAnnotationObject[]) => { + const annotation = pageAnnotations?.find((ann) => ann.id === annotationId) as StampAnnotation | undefined; + if (annotation && annotation.type === PdfAnnotationSubtype.STAMP) { + const stampAnnotation = annotation as StampAnnotation; + if (stampAnnotation.imageSrc) { + // Store image data before deletion + storeImageData(annotationId, stampAnnotation.imageSrc); + } + } + }) + .catch(console.error); } // Delete specific annotation by ID @@ -212,7 +230,7 @@ export const SignatureAPIBridge = forwardRef(function SignatureAPI annotationApi.setActiveTool(null); }, - getPageAnnotations: async (pageIndex: number): Promise => { + getPageAnnotations: async (pageIndex: number): Promise => { if (!annotationApi || !annotationApi.getPageAnnotations) { console.warn('getPageAnnotations not available'); return []; diff --git a/frontend/src/core/components/viewer/ThumbnailSidebar.tsx b/frontend/src/core/components/viewer/ThumbnailSidebar.tsx index 510a6c1a6..7c5079460 100644 --- a/frontend/src/core/components/viewer/ThumbnailSidebar.tsx +++ b/frontend/src/core/components/viewer/ThumbnailSidebar.tsx @@ -59,7 +59,7 @@ export function ThumbnailSidebar({ visible, onToggle: _onToggle, activeFileIndex ...prev, [pageIndex]: thumbUrl })); - }).catch((error: any) => { + }).catch((error: unknown) => { console.error('Failed to generate thumbnail for page', pageIndex + 1, error); setThumbnails(prev => ({ ...prev, diff --git a/frontend/src/core/components/viewer/viewerTypes.ts b/frontend/src/core/components/viewer/viewerTypes.ts index f8149dce7..200424960 100644 --- a/frontend/src/core/components/viewer/viewerTypes.ts +++ b/frontend/src/core/components/viewer/viewerTypes.ts @@ -1,3 +1,5 @@ +import type { PdfAnnotationObject } from '@embedpdf/models'; + export interface SignatureAPI { addImageSignature: ( signatureData: string, @@ -13,7 +15,7 @@ export interface SignatureAPI { deleteAnnotation: (annotationId: string, pageIndex: number) => void; updateDrawSettings: (color: string, size: number) => void; deactivateTools: () => void; - getPageAnnotations: (pageIndex: number) => Promise; + getPageAnnotations: (pageIndex: number) => Promise; } export interface HistoryAPI { @@ -22,3 +24,12 @@ export interface HistoryAPI { canUndo: () => boolean; canRedo: () => boolean; } + +export interface AnnotationEvent { + type: 'create' | 'update' | 'delete' | 'loaded'; + annotation?: PdfAnnotationObject; + pageIndex?: number; + committed?: boolean; + annotations?: PdfAnnotationObject[]; + total?: number; +} diff --git a/frontend/src/core/contexts/ViewerContext.tsx b/frontend/src/core/contexts/ViewerContext.tsx index 8e0bea44a..294308d4a 100644 --- a/frontend/src/core/contexts/ViewerContext.tsx +++ b/frontend/src/core/contexts/ViewerContext.tsx @@ -24,8 +24,8 @@ interface PanAPIWrapper { interface SelectionAPIWrapper { copyToClipboard: () => void; - getSelectedText: () => string | any; - getFormattedSelection: () => any; + getSelectedText: () => string | null; + getFormattedSelection: () => string | null; } interface SpreadAPIWrapper { @@ -42,7 +42,7 @@ interface RotationAPIWrapper { } interface SearchAPIWrapper { - search: (query: string) => Promise; + search: (query: string) => Promise; clear: () => void; next: () => void; previous: () => void; @@ -179,8 +179,8 @@ interface ViewerContextType { selectionActions: { copyToClipboard: () => void; - getSelectedText: () => string; - getFormattedSelection: () => unknown; + getSelectedText: () => string | null; + getFormattedSelection: () => string | null; }; spreadActions: { @@ -435,17 +435,17 @@ export const ViewerProvider: React.FC = ({ children }) => { api.copyToClipboard(); } }, - getSelectedText: () => { + getSelectedText: (): string | null => { const api = bridgeRefs.current.selection?.api; if (api?.getSelectedText) { - return api.getSelectedText(); + return api.getSelectedText() ?? null; } - return ''; + return null; }, - getFormattedSelection: () => { + getFormattedSelection: (): string | null => { const api = bridgeRefs.current.selection?.api; if (api?.getFormattedSelection) { - return api.getFormattedSelection(); + return api.getFormattedSelection() ?? null; } return null; } diff --git a/frontend/src/core/contexts/file/fileActions.ts b/frontend/src/core/contexts/file/fileActions.ts index 5b4d1d2d9..d97610dbc 100644 --- a/frontend/src/core/contexts/file/fileActions.ts +++ b/frontend/src/core/contexts/file/fileActions.ts @@ -479,7 +479,7 @@ export async function undoConsumeFiles( outputFileIds: FileId[], filesRef: React.MutableRefObject>, dispatch: React.Dispatch, - indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise; deleteFile: (fileId: FileId) => Promise } | null + indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise; deleteFile: (fileId: FileId) => Promise } | null ): Promise { if (DEBUG) console.log(`📄 undoConsumeFiles: Restoring ${inputStirlingFileStubs.length} input files, removing ${outputFileIds.length} output files`); diff --git a/frontend/src/core/data/toolsTaxonomy.ts b/frontend/src/core/data/toolsTaxonomy.ts index 0733d9fcc..290955012 100644 --- a/frontend/src/core/data/toolsTaxonomy.ts +++ b/frontend/src/core/data/toolsTaxonomy.ts @@ -52,9 +52,9 @@ export type ToolRegistryEntry = { // Workbench type for navigation workbench?: WorkbenchType; // Operation configuration for automation - operationConfig?: ToolOperationConfig; + operationConfig?: ToolOperationConfig; // Settings component for automation configuration - automationSettings: React.ComponentType | null; + automationSettings: React.ComponentType | null; // Whether this tool supports automation (defaults to true) supportsAutomate?: boolean; // Synonyms for search (optional) diff --git a/frontend/src/core/data/useTranslatedToolRegistry.tsx b/frontend/src/core/data/useTranslatedToolRegistry.tsx index 0860b5d96..a9b83d58f 100644 --- a/frontend/src/core/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/core/data/useTranslatedToolRegistry.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useMemo, type ComponentType } from "react"; import LocalIcon from "@app/components/shared/LocalIcon"; import { useTranslation } from "react-i18next"; import SplitPdfPanel from "@app/tools/Split"; @@ -121,6 +121,9 @@ import ValidateSignature from "@app/tools/ValidateSignature"; import Automate from "@app/tools/Automate"; import { CONVERT_SUPPORTED_FORMATS } from "@app/constants/convertSupportedFornats"; +const toAutomationSettings = (component: ComponentType): ComponentType => + component as unknown as ComponentType; + export interface TranslatedToolCatalog { allTools: ToolRegistry; regularTools: RegularToolRegistry; @@ -161,7 +164,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["merge-pdfs"], operationConfig: mergeOperationConfig, - automationSettings: MergeSettings, + automationSettings: toAutomationSettings(MergeSettings), synonyms: getSynonyms(t, "merge") }, // Signing @@ -176,7 +179,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["cert-sign"], operationConfig: certSignOperationConfig, - automationSettings: CertSignAutomationSettings, + automationSettings: toAutomationSettings(CertSignAutomationSettings), }, sign: { icon: , @@ -186,7 +189,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.SIGNING, operationConfig: signOperationConfig, - automationSettings: SignSettings, // TODO:: not all settings shown, suggested next tools shown + automationSettings: toAutomationSettings(SignSettings), // TODO:: not all settings shown, suggested next tools shown synonyms: getSynonyms(t, "sign"), supportsAutomate: false, //TODO make support Sign }, @@ -203,7 +206,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["add-password"], operationConfig: addPasswordOperationConfig, - automationSettings: AddPasswordSettings, + automationSettings: toAutomationSettings(AddPasswordSettings), synonyms: getSynonyms(t, "addPassword") }, watermark: { @@ -216,7 +219,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.DOCUMENT_SECURITY, endpoints: ["add-watermark"], operationConfig: addWatermarkOperationConfig, - automationSettings: AddWatermarkSingleStepSettings, + automationSettings: toAutomationSettings(AddWatermarkSingleStepSettings), synonyms: getSynonyms(t, "watermark") }, addStamp: { @@ -230,7 +233,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["add-stamp"], operationConfig: addStampOperationConfig, - automationSettings: AddStampAutomationSettings, + automationSettings: toAutomationSettings(AddStampAutomationSettings), }, sanitize: { icon: , @@ -242,7 +245,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"), endpoints: ["sanitize-pdf"], operationConfig: sanitizeOperationConfig, - automationSettings: SanitizeSettings, + automationSettings: toAutomationSettings(SanitizeSettings), synonyms: getSynonyms(t, "sanitize") }, flatten: { @@ -255,7 +258,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["flatten"], operationConfig: flattenOperationConfig, - automationSettings: FlattenSettings, + automationSettings: toAutomationSettings(FlattenSettings), synonyms: getSynonyms(t, "flatten") }, unlockPDFForms: { @@ -281,7 +284,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["add-password"], operationConfig: changePermissionsOperationConfig, - automationSettings: ChangePermissionsSettings, + automationSettings: toAutomationSettings(ChangePermissionsSettings), synonyms: getSynonyms(t, "changePermissions"), }, getPdfInfo: { @@ -335,7 +338,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["update-metadata"], operationConfig: changeMetadataOperationConfig, - automationSettings: ChangeMetadataSingleStep, + automationSettings: toAutomationSettings(ChangeMetadataSingleStep), synonyms: getSynonyms(t, "changeMetadata") }, // Page Formatting @@ -350,7 +353,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["crop"], operationConfig: cropOperationConfig, - automationSettings: CropAutomationSettings, + automationSettings: toAutomationSettings(CropAutomationSettings), }, rotate: { icon: , @@ -362,7 +365,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["rotate-pdf"], operationConfig: rotateOperationConfig, - automationSettings: RotateAutomationSettings, + automationSettings: toAutomationSettings(RotateAutomationSettings), synonyms: getSynonyms(t, "rotate") }, split: { @@ -373,7 +376,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, operationConfig: splitOperationConfig, - automationSettings: SplitAutomationSettings, + automationSettings: toAutomationSettings(SplitAutomationSettings), synonyms: getSynonyms(t, "split") }, reorganizePages: { @@ -402,7 +405,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["scale-pages"], operationConfig: adjustPageScaleOperationConfig, - automationSettings: AdjustPageScaleSettings, + automationSettings: toAutomationSettings(AdjustPageScaleSettings), synonyms: getSynonyms(t, "scalePages") }, addPageNumbers: { @@ -412,7 +415,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, - automationSettings: AddPageNumbersAutomationSettings, + automationSettings: toAutomationSettings(AddPageNumbersAutomationSettings), maxFiles: -1, endpoints: ["add-page-numbers"], operationConfig: addPageNumbersOperationConfig, @@ -427,7 +430,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["multi-page-layout"], - automationSettings: PageLayoutSettings, + automationSettings: toAutomationSettings(PageLayoutSettings), synonyms: getSynonyms(t, "pageLayout") }, bookletImposition: { @@ -435,7 +438,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { name: t("home.bookletImposition.title", "Booklet Imposition"), component: BookletImposition, operationConfig: bookletImpositionOperationConfig, - automationSettings: BookletImpositionSettings, + automationSettings: toAutomationSettings(BookletImpositionSettings), description: t("home.bookletImposition.desc", "Create booklets with proper page ordering and multi-page layout for printing and binding"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, @@ -466,7 +469,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: 1, endpoints: ["add-attachments"], operationConfig: addAttachmentsOperationConfig, - automationSettings: AddAttachmentsSettings, + automationSettings: toAutomationSettings(AddAttachmentsSettings), }, // Extraction @@ -491,7 +494,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["extract-images"], operationConfig: extractImagesOperationConfig, - automationSettings: ExtractImagesSettings, + automationSettings: toAutomationSettings(ExtractImagesSettings), synonyms: getSynonyms(t, "extractImages") }, @@ -508,7 +511,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { endpoints: ["remove-pages"], synonyms: getSynonyms(t, "removePages"), operationConfig: removePagesOperationConfig, - automationSettings: RemovePagesSettings, + automationSettings: toAutomationSettings(RemovePagesSettings), }, removeBlanks: { icon: , @@ -521,7 +524,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { endpoints: ["remove-blanks"], synonyms: getSynonyms(t, "removeBlanks"), operationConfig: removeBlanksOperationConfig, - automationSettings: RemoveBlanksSettings, + automationSettings: toAutomationSettings(RemoveBlanksSettings), }, removeAnnotations: { icon: , @@ -558,7 +561,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { endpoints: ["remove-password"], maxFiles: -1, operationConfig: removePasswordOperationConfig, - automationSettings: RemovePasswordSettings, + automationSettings: toAutomationSettings(RemovePasswordSettings), synonyms: getSynonyms(t, "removePassword") }, removeCertSign: { @@ -617,7 +620,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.ADVANCED_FORMATTING, operationConfig: adjustContrastOperationConfig, - automationSettings: AdjustContrastSingleStepSettings, + automationSettings: toAutomationSettings(AdjustContrastSingleStepSettings), synonyms: getSynonyms(t, "adjustContrast"), }, repair: { @@ -643,7 +646,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["extract-image-scans"], operationConfig: scannerImageSplitOperationConfig, - automationSettings: ScannerImageSplitSettings, + automationSettings: toAutomationSettings(ScannerImageSplitSettings), synonyms: getSynonyms(t, "ScannerImageSplit"), }, overlayPdfs: { @@ -655,7 +658,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.ADVANCED_FORMATTING, operationConfig: overlayPdfsOperationConfig, synonyms: getSynonyms(t, "overlay-pdfs"), - automationSettings: OverlayPdfsSettings + automationSettings: toAutomationSettings(OverlayPdfsSettings) }, replaceColor: { icon: , @@ -667,7 +670,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["replace-invert-pdf"], operationConfig: replaceColorOperationConfig, - automationSettings: ReplaceColorSettings, + automationSettings: toAutomationSettings(ReplaceColorSettings), synonyms: getSynonyms(t, "replaceColor"), }, addImage: { @@ -784,7 +787,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, operationConfig: compressOperationConfig, - automationSettings: CompressSettings, + automationSettings: toAutomationSettings(CompressSettings), synonyms: getSynonyms(t, "compress") }, convert: { @@ -814,7 +817,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { ], operationConfig: convertOperationConfig, - automationSettings: ConvertSettings, + automationSettings: toAutomationSettings(ConvertSettings), synonyms: getSynonyms(t, "convert") }, @@ -827,7 +830,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, operationConfig: ocrOperationConfig, - automationSettings: OCRSettings, + automationSettings: toAutomationSettings(OCRSettings), synonyms: getSynonyms(t, "ocr") }, redact: { @@ -840,7 +843,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["auto-redact"], operationConfig: redactOperationConfig, - automationSettings: RedactSingleStepSettings, + automationSettings: toAutomationSettings(RedactSingleStepSettings), synonyms: getSynonyms(t, "redact") }, }; diff --git a/frontend/src/core/hooks/tools/automate/useAutomationForm.ts b/frontend/src/core/hooks/tools/automate/useAutomationForm.ts index c86d112d6..69487b826 100644 --- a/frontend/src/core/hooks/tools/automate/useAutomationForm.ts +++ b/frontend/src/core/hooks/tools/automate/useAutomationForm.ts @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { AutomationTool, AutomationConfig, AutomationMode } from '@app/types/automation'; +import { AutomationTool, AutomationConfig, AutomationMode, AutomationParameters } from '@app/types/automation'; import { AUTOMATION_CONSTANTS } from '@app/constants/automation'; import { ToolRegistry } from '@app/data/toolsTaxonomy'; import { ToolId } from "@app/types/toolId"; @@ -21,14 +21,14 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us const [selectedTools, setSelectedTools] = useState([]); const getToolName = useCallback((operation: string) => { - const tool = toolRegistry?.[operation as ToolId] as any; + const tool = toolRegistry?.[operation as ToolId]; return tool?.name || t(`tools.${operation}.name`, operation); }, [toolRegistry, t]); - const getToolDefaultParameters = useCallback((operation: string): Record => { + const getToolDefaultParameters = useCallback((operation: string): AutomationParameters => { const config = toolRegistry[operation as ToolId]?.operationConfig; if (config?.defaultParameters) { - return { ...config.defaultParameters }; + return { ...(config.defaultParameters as AutomationParameters) }; } return {}; }, [toolRegistry]); diff --git a/frontend/src/core/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation.ts b/frontend/src/core/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation.ts index 4078b2b07..abdd8ee40 100644 --- a/frontend/src/core/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation.ts +++ b/frontend/src/core/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation.ts @@ -52,7 +52,7 @@ const removeAnnotationsProcessor = async (_parameters: RemoveAnnotationsParamete try { const catalog = pdfDoc.context.lookup(pdfDoc.context.trailerInfo.Root); if (catalog && 'has' in catalog && 'delete' in catalog) { - const catalogDict = catalog as any; + const catalogDict = catalog as PDFDict; if (catalogDict.has(PDFName.of('AcroForm'))) { catalogDict.delete(PDFName.of('AcroForm')); } @@ -93,4 +93,4 @@ export const useRemoveAnnotationsOperation = () => { ...removeAnnotationsOperationConfig, getErrorMessage: createStandardErrorHandler(t('removeAnnotations.error.failed', 'An error occurred while removing annotations from the PDF.')) }); -}; \ No newline at end of file +}; diff --git a/frontend/src/core/hooks/tools/removePassword/useRemovePasswordOperation.test.ts b/frontend/src/core/hooks/tools/removePassword/useRemovePasswordOperation.test.ts index f29ba1511..8e2d7da9d 100644 --- a/frontend/src/core/hooks/tools/removePassword/useRemovePasswordOperation.test.ts +++ b/frontend/src/core/hooks/tools/removePassword/useRemovePasswordOperation.test.ts @@ -76,7 +76,7 @@ describe('useRemovePasswordOperation', () => { }; const testFile = new File(['test content'], 'test.pdf', { type: 'application/pdf' }); - const formData = buildFormData(testParameters, testFile as any); + const formData = buildFormData(testParameters, testFile); // Verify the form data contains the file expect(formData.get('fileInput')).toBe(testFile); diff --git a/frontend/src/core/hooks/tools/shared/useToolOperation.ts b/frontend/src/core/hooks/tools/shared/useToolOperation.ts index 0f18543de..ae309923e 100644 --- a/frontend/src/core/hooks/tools/shared/useToolOperation.ts +++ b/frontend/src/core/hooks/tools/shared/useToolOperation.ts @@ -16,6 +16,10 @@ import { ToolId } from '@app/types/toolId'; // Re-export for backwards compatibility export type { ProcessingProgress, ResponseHandler }; +type BivariantHandler = { + bivarianceHack(...args: TArgs): TResult; +}['bivarianceHack']; + export enum ToolType { singleFile, multiFile, @@ -62,10 +66,10 @@ export interface SingleFileToolOperationConfig extends BaseToolOperatio toolType: ToolType.singleFile; /** Builds FormData for API request. */ - buildFormData: ((params: TParams, file: File) => FormData); + buildFormData: BivariantHandler<[TParams, File], FormData>; /** API endpoint for the operation. Can be static string or function for dynamic routing. */ - endpoint: string | ((params: TParams) => string); + endpoint: string | BivariantHandler<[TParams], string>; customProcessor?: undefined; } @@ -78,10 +82,10 @@ export interface MultiFileToolOperationConfig extends BaseToolOperation filePrefix: string; /** Builds FormData for API request. */ - buildFormData: ((params: TParams, files: File[]) => FormData); + buildFormData: BivariantHandler<[TParams, File[]], FormData>; /** API endpoint for the operation. Can be static string or function for dynamic routing. */ - endpoint: string | ((params: TParams) => string); + endpoint: string | BivariantHandler<[TParams], string>; customProcessor?: undefined; } @@ -98,7 +102,7 @@ export interface CustomToolOperationConfig extends BaseToolOperationCon * This tool handles all API calls, response processing, and file creation. * Use for tools with complex routing logic or non-standard processing requirements. */ - customProcessor: (params: TParams, files: File[]) => Promise; + customProcessor: BivariantHandler<[TParams, File[]], Promise>; } export type ToolOperationConfig = SingleFileToolOperationConfig | MultiFileToolOperationConfig | CustomToolOperationConfig; @@ -119,11 +123,11 @@ export interface ToolOperationHook { progress: ProcessingProgress | null; // Actions - executeOperation: (params: TParams, selectedFiles: StirlingFile[]) => Promise; - resetResults: () => void; - clearError: () => void; - cancelOperation: () => void; - undoOperation: () => Promise; + executeOperation(params: TParams, selectedFiles: StirlingFile[]): Promise; + resetResults(): void; + clearError(): void; + cancelOperation(): void; + undoOperation(): Promise; } // Re-export for backwards compatibility diff --git a/frontend/src/core/hooks/useAdminSettings.ts b/frontend/src/core/hooks/useAdminSettings.ts index 6432fe31d..08a1f79fc 100644 --- a/frontend/src/core/hooks/useAdminSettings.ts +++ b/frontend/src/core/hooks/useAdminSettings.ts @@ -2,7 +2,7 @@ import { useState } from 'react'; import apiClient from '@app/services/apiClient'; import { mergePendingSettings, isFieldPending, hasPendingChanges, SettingsWithPending } from '@app/utils/settingsPendingHelper'; -type SettingsRecord = Record; +export type SettingsRecord = Record; type CombinedSettings = T & SettingsRecord; type PendingSettings = SettingsWithPending> & CombinedSettings; diff --git a/frontend/src/core/hooks/useFileWithUrl.ts b/frontend/src/core/hooks/useFileWithUrl.ts index ce0d5945e..f701248ad 100644 --- a/frontend/src/core/hooks/useFileWithUrl.ts +++ b/frontend/src/core/hooks/useFileWithUrl.ts @@ -18,13 +18,7 @@ export function useFileWithUrl(file: File | Blob | null): { file: File | Blob; u try { const url = URL.createObjectURL(file); - // Return object with cleanup function - const result = { file, url }; - - // Store cleanup function for later use - (result as any)._cleanup = () => URL.revokeObjectURL(url); - - return result; + return { file, url }; } catch (error) { console.error('useFileWithUrl: Failed to create object URL:', error, file); return null; diff --git a/frontend/src/core/hooks/useSuggestedTools.ts b/frontend/src/core/hooks/useSuggestedTools.ts index 857108ed4..b2ac213af 100644 --- a/frontend/src/core/hooks/useSuggestedTools.ts +++ b/frontend/src/core/hooks/useSuggestedTools.ts @@ -14,7 +14,7 @@ import TextFieldsIcon from '@mui/icons-material/TextFields'; export interface SuggestedTool { id: ToolId; title: string; - icon: React.ComponentType; + icon: React.ComponentType; href: string; onClick: (e: React.MouseEvent) => void; } diff --git a/frontend/src/core/pages/HomePage.tsx b/frontend/src/core/pages/HomePage.tsx index 683f76a08..3f69c28de 100644 --- a/frontend/src/core/pages/HomePage.tsx +++ b/frontend/src/core/pages/HomePage.tsx @@ -268,7 +268,7 @@ export default function HomePage({ openedFile }: HomePageProps = {}) { {t('quickAccess.config', 'Config')} - + setConfigModalOpen(false)} @@ -285,7 +285,7 @@ export default function HomePage({ openedFile }: HomePageProps = {}) { - + )} diff --git a/frontend/src/core/services/fileAnalyzer.ts b/frontend/src/core/services/fileAnalyzer.ts index e168e530b..e8347ec4a 100644 --- a/frontend/src/core/services/fileAnalyzer.ts +++ b/frontend/src/core/services/fileAnalyzer.ts @@ -72,7 +72,7 @@ export class FileAnalyzer { }); const pageCount = pdf.numPages; - const isEncrypted = (pdf as any).isEncrypted; + const isEncrypted = Boolean((pdf as { isEncrypted?: boolean }).isEncrypted); // Clean up using worker manager pdfWorkerManager.destroyDocument(pdf); diff --git a/frontend/src/core/services/pdfProcessingService.ts b/frontend/src/core/services/pdfProcessingService.ts index 02d29898c..3ca4ab214 100644 --- a/frontend/src/core/services/pdfProcessingService.ts +++ b/frontend/src/core/services/pdfProcessingService.ts @@ -2,6 +2,7 @@ import { ProcessedFile, ProcessingState, PDFPage } from '@app/types/processing'; import { ProcessingCache } from '@app/services/processingCache'; import { pdfWorkerManager } from '@app/services/pdfWorkerManager'; import { createQuickKey } from '@app/types/fileContext'; +import { ProcessingErrorHandler } from '@app/services/processingErrorHandler'; export class PDFProcessingService { private static instance: PDFProcessingService; @@ -78,7 +79,7 @@ export class PDFProcessingService { } catch (error) { console.error('Processing failed for', file.name, ':', error); state.status = 'error'; - state.error = (error instanceof Error ? error.message : 'Unknown error') as any; + state.error = ProcessingErrorHandler.createProcessingError(error); this.notifyListeners(); // Remove failed processing after delay diff --git a/frontend/src/core/services/teamService.ts b/frontend/src/core/services/teamService.ts index 36434a293..8b152d9e3 100644 --- a/frontend/src/core/services/teamService.ts +++ b/frontend/src/core/services/teamService.ts @@ -1,4 +1,5 @@ import apiClient from '@app/services/apiClient'; +import type { AxiosRequestConfig } from 'axios'; export interface Team { id: number; @@ -16,7 +17,9 @@ export interface TeamMember { id: number; name: string; }; - lastRequest?: Date | null; + lastRequest?: number | null; + rolesAsString?: string; + authenticationType?: string; } export interface TeamDetailsResponse { @@ -25,6 +28,17 @@ export interface TeamDetailsResponse { availableUsers: TeamMember[]; } +interface TeamDetailsData { + team: Team; + teamUsers: TeamMember[]; + availableUsers: TeamMember[]; + userLastRequest?: Record; +} + +const suppressErrorToastConfig: AxiosRequestConfig & { suppressErrorToast: boolean } = { + suppressErrorToast: true, +}; + /** * Team Management Service * Provides functions to interact with team-related backend APIs @@ -41,7 +55,7 @@ export const teamService = { /** * Get team details including members */ - async getTeamDetails(teamId: number): Promise { + async getTeamDetails(teamId: number): Promise { const response = await apiClient.get(`/api/v1/proprietary/ui-data/teams/${teamId}`); return response.data; }, @@ -52,9 +66,7 @@ export const teamService = { async createTeam(name: string): Promise { const formData = new FormData(); formData.append('name', name); - await apiClient.post('/api/v1/team/create', formData, { - suppressErrorToast: true, - } as any); + await apiClient.post('/api/v1/team/create', formData, suppressErrorToastConfig); }, /** @@ -64,9 +76,7 @@ export const teamService = { const formData = new FormData(); formData.append('teamId', teamId.toString()); formData.append('newName', newName); - await apiClient.post('/api/v1/team/rename', formData, { - suppressErrorToast: true, - } as any); + await apiClient.post('/api/v1/team/rename', formData, suppressErrorToastConfig); }, /** @@ -75,9 +85,7 @@ export const teamService = { async deleteTeam(teamId: number): Promise { const formData = new FormData(); formData.append('teamId', teamId.toString()); - await apiClient.post('/api/v1/team/delete', formData, { - suppressErrorToast: true, - } as any); + await apiClient.post('/api/v1/team/delete', formData, suppressErrorToastConfig); }, /** @@ -87,9 +95,7 @@ export const teamService = { const formData = new FormData(); formData.append('teamId', teamId.toString()); formData.append('userId', userId.toString()); - await apiClient.post('/api/v1/team/addUser', formData, { - suppressErrorToast: true, - } as any); + await apiClient.post('/api/v1/team/addUser', formData, suppressErrorToastConfig); }, /** @@ -100,8 +106,6 @@ export const teamService = { formData.append('username', username); formData.append('role', currentRole); formData.append('teamId', teamId.toString()); - await apiClient.post('/api/v1/user/admin/changeRole', formData, { - suppressErrorToast: true, - } as any); + await apiClient.post('/api/v1/user/admin/changeRole', formData, suppressErrorToastConfig); }, }; diff --git a/frontend/src/core/types/parameters.ts b/frontend/src/core/types/parameters.ts index 91355cb81..166c83877 100644 --- a/frontend/src/core/types/parameters.ts +++ b/frontend/src/core/types/parameters.ts @@ -1,6 +1,16 @@ // Base parameter interfaces for reusable patterns -// Base interface that all tool parameters should extend -// Provides a foundation for adding common properties across all tools -// Examples of future additions: userId, sessionId, commonFlags, etc. -export type BaseParameters = object +/** + * Base interface that all tool parameters should extend. + * Provides a foundation for adding common properties across all tools + * (e.g., userId, sessionId, common flags). + */ +export type BaseParameters = object; + +/** + * Generic handler for updating individual parameter values. + */ +export type ParameterUpdater = ( + parameter: K, + value: T[K] +) => void; diff --git a/frontend/src/core/types/tool.ts b/frontend/src/core/types/tool.ts index a6a41eefe..646fdc3fc 100644 --- a/frontend/src/core/types/tool.ts +++ b/frontend/src/core/types/tool.ts @@ -21,13 +21,13 @@ export interface AutomationCapableTool { * Static method that returns the operation hook for this tool. * This enables automation to execute the tool programmatically. */ - tool: () => () => ToolOperationHook; + tool: () => () => ToolOperationHook; /** * Static method that returns the default parameters for this tool. * This enables automation creation to initialize tools with proper defaults. */ - getDefaultParameters: () => any; + getDefaultParameters: () => Record; } /** @@ -54,7 +54,7 @@ export interface ToolResult { files?: File[]; error?: string; downloadUrl?: string; - metadata?: Record; + metadata?: Record; } export interface ToolConfiguration { @@ -77,4 +77,3 @@ export interface Tool { } export type ToolRegistry = Record; - diff --git a/frontend/src/core/utils/convertUtils.test.ts b/frontend/src/core/utils/convertUtils.test.ts index 7441bbcef..b833a96a5 100644 --- a/frontend/src/core/utils/convertUtils.test.ts +++ b/frontend/src/core/utils/convertUtils.test.ts @@ -307,13 +307,13 @@ describe('convertUtils', () => { test('should handle null and undefined inputs gracefully', () => { // Note: TypeScript prevents these, but test runtime behavior for robustness // The current implementation handles these gracefully by returning falsy values - expect(getEndpointName(null as any, null as any)).toBe(''); - expect(getEndpointUrl(undefined as any, undefined as any)).toBe(''); - expect(isConversionSupported(null as any, null as any)).toBe(false); + expect(getEndpointName(null as unknown as string, null as unknown as string)).toBe(''); + expect(getEndpointUrl(undefined as unknown as string, undefined as unknown as string)).toBe(''); + expect(isConversionSupported(null as unknown as string, null as unknown as string)).toBe(false); // isImageFormat will throw because it calls toLowerCase() on null/undefined - expect(() => isImageFormat(null as any)).toThrow(); - expect(() => isImageFormat(undefined as any)).toThrow(); + expect(() => isImageFormat(null as unknown as string)).toThrow(); + expect(() => isImageFormat(undefined as unknown as string)).toThrow(); }); test('should handle special characters in file extensions', () => { @@ -331,4 +331,4 @@ describe('convertUtils', () => { expect(getEndpointName(longExtension, 'pdf')).toBe('file-to-pdf'); // Fallback to file to pdf }); }); -}); \ No newline at end of file +}); diff --git a/frontend/src/core/utils/signatureFlattening.ts b/frontend/src/core/utils/signatureFlattening.ts index 2b3a9cefe..d78a87a90 100644 --- a/frontend/src/core/utils/signatureFlattening.ts +++ b/frontend/src/core/utils/signatureFlattening.ts @@ -4,6 +4,7 @@ import { createProcessedFile, createChildStub } from '@app/contexts/file/fileAct import { createStirlingFile, StirlingFile, FileId, StirlingFileStub } from '@app/types/fileContext'; import type { SignatureAPI } from '@app/components/viewer/viewerTypes'; import type React from 'react'; +import type { PdfAnnotationObject } from '@embedpdf/models'; interface MinimalFileContextSelectors { getAllFileIds: () => FileId[]; @@ -40,28 +41,22 @@ type AnnotationRect = { height?: number; }; -type SessionAnnotation = { - id?: string; +type SessionAnnotation = PdfAnnotationObject & { rect?: AnnotationRect; bounds?: AnnotationRect; rectangle?: AnnotationRect; position?: AnnotationRect; - imageData?: unknown; - appearance?: unknown; - stampData?: unknown; - imageSrc?: unknown; - contents?: unknown; - data?: unknown; - type?: number; - [key: string]: unknown; + imageData?: string; + appearance?: string; + stampData?: string; + imageSrc?: string; + contents?: string; + data?: string; }; const getAnnotationRect = (annotation: SessionAnnotation): AnnotationRect | undefined => annotation.rect ?? annotation.bounds ?? annotation.rectangle ?? annotation.position; -const isSessionAnnotationArray = (value: unknown): value is SessionAnnotation[] => - Array.isArray(value); - export async function flattenSignatures(options: SignatureFlatteningOptions): Promise { const { signatureApiRef, getImageData, exportActions, selectors, originalFile, getScrollState, activeFileIndex } = options; @@ -79,23 +74,24 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) { try { const pageAnnotations = await signatureApiRef.current.getPageAnnotations(pageIndex); - if (isSessionAnnotationArray(pageAnnotations) && pageAnnotations.length > 0) { + if (Array.isArray(pageAnnotations) && pageAnnotations.length > 0) { // Filter to only include annotations added in this session const sessionAnnotations = pageAnnotations.filter((annotation): annotation is SessionAnnotation => { if (!annotation || typeof annotation !== 'object') { return false; } + const extendedAnnotation = annotation as SessionAnnotation; // Check if this annotation has stored image data (indicates it was added this session) - const storedImageData = annotation.id ? getImageData(annotation.id) : undefined; + const storedImageData = extendedAnnotation.id ? getImageData(extendedAnnotation.id) : undefined; // Also check if it has image data directly in the annotation (new signatures) const directImageData = [ - annotation.imageData, - annotation.appearance, - annotation.stampData, - annotation.imageSrc, - annotation.contents, - annotation.data + extendedAnnotation.imageData, + extendedAnnotation.appearance, + extendedAnnotation.stampData, + extendedAnnotation.imageSrc, + extendedAnnotation.contents, + extendedAnnotation.data ].find((value): value is string => typeof value === 'string'); return Boolean( @@ -272,9 +268,9 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr console.error('Failed to render image annotation:', imageError); } } else { - const textContent = typeof annotation.content === 'string' - ? annotation.content - : (typeof annotation.text === 'string' ? annotation.text : undefined); + const textContent = typeof annotation.contents === 'string' + ? annotation.contents + : (typeof annotation.data === 'string' ? annotation.data : undefined); if (textContent) { console.warn('Rendering text annotation instead'); diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index c52fb3753..ff958d64e 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -24,7 +24,7 @@ function updatePosthogConsent(){ if(typeof(posthog) == "undefined" || !posthog.__loaded) { return; } - const optIn = (window.CookieConsent as any)?.acceptedService?.('posthog', 'analytics') || false; + const optIn = window.CookieConsent?.acceptedService?.('posthog', 'analytics') ?? false; if (optIn) { posthog.opt_in_capturing(); } else { diff --git a/frontend/src/proprietary/auth/springAuthClient.ts b/frontend/src/proprietary/auth/springAuthClient.ts index 8567eb654..47054f65c 100644 --- a/frontend/src/proprietary/auth/springAuthClient.ts +++ b/frontend/src/proprietary/auth/springAuthClient.ts @@ -16,7 +16,7 @@ export interface User { enabled?: boolean; is_anonymous?: boolean; isFirstLogin?: boolean; - app_metadata?: Record; + app_metadata?: Record; } export interface Session { @@ -217,7 +217,7 @@ class SpringAuthClient { */ async signInWithOAuth(params: { provider: 'github' | 'google' | 'apple' | 'azure'; - options?: { redirectTo?: string; queryParams?: Record }; + options?: { redirectTo?: string; queryParams?: Record }; }): Promise<{ error: AuthError | null }> { try { // Redirect to Spring OAuth2 endpoint (Vite will proxy to backend) diff --git a/frontend/src/proprietary/components/shared/DividerWithText.tsx b/frontend/src/proprietary/components/shared/DividerWithText.tsx index 888f4acfb..80e79bb94 100644 --- a/frontend/src/proprietary/components/shared/DividerWithText.tsx +++ b/frontend/src/proprietary/components/shared/DividerWithText.tsx @@ -12,7 +12,9 @@ interface TextDividerProps { export default function DividerWithText({ text, className = '', style, variant = 'default', respondsToDarkMode = true, opacity }: TextDividerProps) { const variantClass = variant === 'subcategory' ? 'subcategory' : ''; const themeClass = respondsToDarkMode ? '' : 'force-light'; - const styleWithOpacity = opacity !== undefined ? { ...(style || {}), ['--text-divider-opacity' as any]: opacity } : style; + const styleWithOpacity = opacity !== undefined + ? { ...(style || {}), '--text-divider-opacity': opacity } + : style; if (text) { return ( diff --git a/frontend/src/proprietary/routes/login/OAuthButtons.tsx b/frontend/src/proprietary/routes/login/OAuthButtons.tsx index e7fcb1b06..334426b87 100644 --- a/frontend/src/proprietary/routes/login/OAuthButtons.tsx +++ b/frontend/src/proprietary/routes/login/OAuthButtons.tsx @@ -1,8 +1,17 @@ import { useTranslation } from 'react-i18next'; import { BASE_PATH } from '@app/constants/app'; +type OAuthProviderId = 'google' | 'github' | 'apple' | 'azure'; + +interface OAuthProviderConfig { + id: OAuthProviderId; + label: string; + file: string; + isDisabled: boolean; +} + // OAuth provider configuration -const oauthProviders = [ +const oauthProviders: ReadonlyArray = [ { id: 'google', label: 'Google', file: 'google.svg', isDisabled: false }, { id: 'github', label: 'GitHub', file: 'github.svg', isDisabled: false }, { id: 'apple', label: 'Apple', file: 'apple.svg', isDisabled: true }, @@ -10,7 +19,7 @@ const oauthProviders = [ ]; interface OAuthButtonsProps { - onProviderClick: (provider: 'github' | 'google' | 'apple' | 'azure') => void + onProviderClick: (provider: OAuthProviderId) => void isSubmitting: boolean layout?: 'vertical' | 'grid' | 'icons' } @@ -27,7 +36,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = ' {enabledProviders.map((p) => (