Fix more any type uses

This commit is contained in:
James Brunton 2025-11-07 13:26:39 +00:00
parent 373f16a86f
commit 614da8d6a8
75 changed files with 700 additions and 517 deletions

View File

@ -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<FileManagerProps> = ({ selectedTool }) => {

View File

@ -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<PDFAnnotationContextValue | undefined>(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<PDFAnnotationProviderProps> = ({
@ -88,4 +89,4 @@ export const usePDFAnnotation = (): PDFAnnotationContextValue => {
throw new Error('usePDFAnnotation must be used within a PDFAnnotationProvider');
}
return context;
};
};

View File

@ -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<BaseAnnotationToolProps> = ({
/>
{/* Tool Content */}
{React.cloneElement(children as React.ReactElement<any>, {
{children({
selectedColor,
signatureData,
onSignatureDataChange: handleSignatureDataChange,
@ -86,4 +94,4 @@ export const BaseAnnotationTool: React.FC<BaseAnnotationToolProps> = ({
/>
</Stack>
);
};
};

View File

@ -12,7 +12,6 @@ export const DrawingTool: React.FC<DrawingToolProps> = ({
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<DrawingToolProps> = ({
onSignatureDataChange={onDrawingChange}
disabled={disabled}
>
<Stack gap="sm">
<DrawingCanvas
selectedColor={selectedColor}
penSize={penSize}
penSizeInput={penSizeInput}
onColorSwatchClick={() => {}} // Color picker handled by BaseAnnotationTool
onPenSizeChange={setPenSize}
onPenSizeInputChange={setPenSizeInput}
onSignatureDataChange={onDrawingChange || (() => {})}
disabled={disabled}
/>
</Stack>
{({ selectedColor, onColorSwatchClick, onSignatureDataChange }) => (
<Stack gap="sm">
<DrawingCanvas
selectedColor={selectedColor}
penSize={penSize}
penSizeInput={penSizeInput}
onColorSwatchClick={onColorSwatchClick}
onPenSizeChange={setPenSize}
onPenSizeInputChange={setPenSizeInput}
onSignatureDataChange={onSignatureDataChange}
disabled={disabled}
/>
</Stack>
)}
</BaseAnnotationTool>
);
};
};

View File

@ -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<ImageToolProps> = ({
onImageChange,
disabled = false
}) => {
const [, setImageData] = useState<string | null>(null);
const readFileAsDataUrl = async (file: File | null): Promise<string | null> => {
if (!file || disabled) {
return null;
}
const handleImageUpload = async (file: File | null) => {
if (file && !disabled) {
try {
const result = await new Promise<string>((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<string>((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<ImageToolProps> = ({
onSignatureDataChange={onImageChange}
disabled={disabled}
>
<Stack gap="sm">
<ImageUploader
onImageChange={handleImageUpload}
disabled={disabled}
label="Upload Image"
placeholder="Select image file"
hint="Upload a PNG, JPG, or other image file to place on the PDF"
/>
</Stack>
{({ onSignatureDataChange }) => (
<Stack gap="sm">
<ImageUploader
onImageChange={async (file) => {
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"
/>
</Stack>
)}
</BaseAnnotationTool>
);
};
};

View File

@ -39,19 +39,21 @@ export const TextTool: React.FC<TextToolProps> = ({
onSignatureDataChange={handleSignatureDataChange}
disabled={disabled}
>
<Stack gap="sm">
<TextInputWithFont
text={text}
onTextChange={handleTextChange}
fontSize={fontSize}
onFontSizeChange={setFontSize}
fontFamily={fontFamily}
onFontFamilyChange={setFontFamily}
disabled={disabled}
label="Text Content"
placeholder="Enter text to place on the PDF"
/>
</Stack>
{() => (
<Stack gap="sm">
<TextInputWithFont
text={text}
onTextChange={handleTextChange}
fontSize={fontSize}
onFontSizeChange={setFontSize}
fontFamily={fontFamily}
onFontFamilyChange={setFontFamily}
disabled={disabled}
label="Text Content"
placeholder="Enter text to place on the PDF"
/>
</Stack>
)}
</BaseAnnotationTool>
);
};
};

View File

@ -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}

View File

@ -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<PageThumbnailProps> = ({
const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null);
const [isHovered, setIsHovered] = useState(false);
const isMobile = useMediaQuery('(max-width: 1024px)');
const dragElementRef = useRef<HTMLDivElement>(null);
const dragElementRef = useRef<DragManagedElement | null>(null);
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
const { openFilesModal } = useFilesModalContext();
@ -132,8 +133,9 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
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<PageThumbnailProps> = ({
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]);

View File

@ -571,7 +571,6 @@ export class InsertFilesCommand extends DOMCommand {
private insertedPages: PDFPage[] = [];
private originalDocument: PDFDocument | null = null;
private fileDataMap = new Map<FileId, ArrayBuffer>(); // Store file data for thumbnail generation
private originalProcessedFile: any = null; // Store original ProcessedFile for undo
private insertedFileMap = new Map<FileId, File>(); // Store inserted files for export
constructor(

View File

@ -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<HTMLElement>;
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<TooltipTriggerProps>;
offset?: number;
maxWidth?: number | string;
minWidth?: number | string;
@ -76,6 +88,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
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<TooltipProps> = ({
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<TooltipProps> = ({
// 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<TooltipProps> = ({
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<TooltipProps> = ({
);
// 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<HTMLElement | null> }).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<HTMLElement | null>).current = node;
}
},
'aria-describedby': open ? tooltipIdRef.current : undefined,
onPointerEnter: handlePointerEnter,

View File

@ -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<string, unknown> {
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> & 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<AdvancedSettingsData> = {};
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<string, any> = {
const deltaSettings: SettingsRecord = {
'system.enableAlphaFunctionality': settings.enableAlphaFunctionality,
'system.maxDPI': settings.maxDPI,
'system.enableUrlToPDF': settings.enableUrlToPDF,

View File

@ -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<string, string | number | boolean | undefined>;
interface ConnectionsSettingsData extends Record<string, unknown> {
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<string, ProviderSettingsMap>;
};
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> & 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<ConnectionsSettingsData> = {};
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<string, any> => {
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<string, any>) => {
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<string, any> = {};
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<string, any> = {};
const deltaSettings: SettingsRecord = {};
if (provider.id === 'saml2') {
deltaSettings['security.saml2.enabled'] = false;

View File

@ -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<string, unknown> {
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> & 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<string, any> = {
const deltaSettings: SettingsRecord = {
'system.datasource.enableCustomDatabase': settings.enableCustomDatabase,
'system.datasource.customDatabaseUrl': settings.customDatabaseUrl,
'system.datasource.username': settings.username,

View File

@ -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<string, unknown> {
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> & 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<string, any> = {};
const deltaSettings: SettingsRecord = {};
if (settings.serverCertificate) {
deltaSettings['system.serverCertificate.enabled'] = settings.serverCertificate.enabled;

View File

@ -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<string, unknown> {
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> & 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<GeneralSettingsData> = {};
if (ui._pending) {
pendingBlock.ui = ui._pending;
}
@ -106,7 +108,7 @@ export default function AdminGeneralSection() {
return result;
},
saveTransformer: (settings) => {
const deltaSettings: Record<string, any> = {
const deltaSettings: SettingsRecord = {
// UI settings
'ui.appNameNavbar': settings.ui.appNameNavbar,
'ui.languages': settings.ui.languages,

View File

@ -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<string, unknown> {
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> & 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<PrivacySettingsData> = {};
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

View File

@ -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<string, unknown> {
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> & 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<SecuritySettingsData> = {};
if (securityPending) {
Object.assign(mergedPending, securityPending);
}
@ -114,7 +116,7 @@ export default function AdminSecuritySection() {
saveTransformer: (settings) => {
const { audit, html, ...securitySettings } = settings;
const deltaSettings: Record<string, any> = {
const deltaSettings: SettingsRecord = {
'premium.enterpriseFeatures.audit.enabled': audit?.enabled,
'premium.enterpriseFeatures.audit.level': audit?.level,
'premium.enterpriseFeatures.audit.retentionDays': audit?.retentionDays

View File

@ -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<string, unknown> | null | undefined;
type ConfigSectionData = Record<string, ConfigDisplayValue>;
const isConfigObject = (value: ConfigDisplayValue): value is Record<string, unknown> => {
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 (
<Stack gap="xs" mb="md">
@ -22,7 +29,7 @@ const Overview: React.FC = () => {
<Badge color={value ? 'green' : 'red'} size="sm">
{value ? 'true' : 'false'}
</Badge>
) : typeof value === 'object' ? (
) : isConfigObject(value) ? (
<Code block>{JSON.stringify(value, null, 2)}</Code>
) : (
String(value) || 'null'

View File

@ -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<ComboboxItem>) => {
const roleOption = option as RoleOptionItem;
return (
<Group gap="sm" wrap="nowrap">
<LocalIcon icon={option.icon} width="1.25rem" height="1.25rem" style={{ flexShrink: 0 }} />
<LocalIcon icon={roleOption.icon} width="1.25rem" height="1.25rem" style={{ flexShrink: 0 }} />
<div style={{ flex: 1 }}>
<Text size="sm" fw={500}>{option.label}</Text>
<Text size="sm" fw={500}>{roleOption.label}</Text>
<Text size="xs" c="dimmed" style={{ whiteSpace: 'normal', lineHeight: 1.4 }}>
{option.description}
{roleOption.description}
</Text>
</div>
</Group>
);
);
};
const teamOptions = teams.map((team) => ({
value: team.id.toString(),

View File

@ -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<string, string | number | boolean | undefined>;
interface ProviderCardProps {
provider: Provider;
isConfigured: boolean;
settings?: Record<string, any>;
onSave?: (settings: Record<string, any>) => 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<Record<string, any>>(settings);
const [localSettings, setLocalSettings] = useState<ProviderSettings>(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<string, any> = {};
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({
<Text size="xs" c="dimmed" mt={4}>{field.description}</Text>
</div>
<Switch
checked={value || false}
checked={typeof value === 'boolean' ? value : Boolean(value)}
onChange={(e) => handleFieldChange(field.key, e.target.checked)}
/>
</div>
@ -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)}
/>
);
}

View File

@ -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<Team | null>(null);
const [teamUsers, setTeamUsers] = useState<User[]>([]);
const [availableUsers, setAvailableUsers] = useState<User[]>([]);
const [teamUsers, setTeamUsers] = useState<TeamMember[]>([]);
const [availableUsers, setAvailableUsers] = useState<TeamMember[]>([]);
const [allTeams, setAllTeams] = useState<Team[]>([]);
const [userLastRequest, setUserLastRequest] = useState<Record<string, number>>({});
const [addMemberModalOpened, setAddMemberModalOpened] = useState(false);
const [changeTeamModalOpened, setChangeTeamModalOpened] = useState(false);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [selectedUser, setSelectedUser] = useState<TeamMember | null>(null);
const [selectedUserId, setSelectedUserId] = useState<string>('');
const [selectedTeamId, setSelectedTeamId] = useState<string>('');
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);

View File

@ -37,8 +37,31 @@ export default function TeamsSection() {
// Form states
const [newTeamName, setNewTeamName] = useState('');
const [renameTeamName, setRenameTeamName] = useState('');
const [selectedUserId, setSelectedUserId] = useState<string>('');
const [renameTeamName, setRenameTeamName] = useState('');
const [selectedUserId, setSelectedUserId] = useState<string>('');
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 });
}
};

View File

@ -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;

View File

@ -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 = ({
</header>
{showDescriptions ? (
<div className="tool-panel__fullscreen-grid tool-panel__fullscreen-grid--detailed">
{recommendedItems.map((item: any) => renderToolItem(item.id, item.tool))}
{recommendedItems.map((item) => renderToolItem(item.id, item.tool))}
</div>
) : (
<div className="tool-panel__fullscreen-list">
{recommendedItems.map((item: any) => renderToolItem(item.id, item.tool))}
{recommendedItems.map((item) => renderToolItem(item.id, item.tool))}
</div>
)}
</section>
@ -250,4 +252,3 @@ const FullscreenToolList = ({
export default FullscreenToolList;

View File

@ -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',

View File

@ -26,7 +26,7 @@ const AddPageNumbersAppearanceSettings = ({
<Select
label={t('addPageNumbers.selectText.2', 'Margin')}
value={parameters.customMargin}
onChange={(v) => onParameterChange('customMargin', (v as any) || 'medium')}
onChange={(value) => onParameterChange('customMargin', (value as AddPageNumbersParameters['customMargin'] | null) ?? 'medium')}
data={[
{ value: 'small', label: t('sizes.small', 'Small') },
{ value: 'medium', label: t('sizes.medium', 'Medium') },
@ -51,7 +51,7 @@ const AddPageNumbersAppearanceSettings = ({
<Select
label={t('addPageNumbers.fontName', 'Font Type')}
value={parameters.fontType}
onChange={(v) => 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' },

View File

@ -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={{

View File

@ -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
/>
<Slider
value={parameters.fontSize}
onChange={(v) => 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
/>
<Slider
value={parameters.rotation}
onChange={(v) => 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
/>
<Slider
value={parameters.opacity}
onChange={(v) => 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
<Select
label={t('AddStampRequest.margin', 'Margin')}
value={parameters.customMargin}
onChange={(v) => onParameterChange('customMargin', (v as any) || 'medium')}
onChange={(value) => onParameterChange('customMargin', (value as AddStampParameters['customMargin'] | null) ?? 'medium')}
data={[
{ value: 'small', label: t('margin.small', 'Small') },
{ value: 'medium', label: t('margin.medium', 'Medium') },

View File

@ -249,8 +249,8 @@ export default function StampPreview({ parameters, onParameterChange, file, show
const scaleY = containerSize.height / heightPts;
const newLeftPts = newLeftPx / scaleX;
const newBottomPts = newBottomPx / scaleY;
onParameterChange('overrideX', newLeftPts as any);
onParameterChange('overrideY', newBottomPts as any);
onParameterChange('overrideX', newLeftPts);
onParameterChange('overrideY', newBottomPts);
}
if (drag.type === 'resize') {
@ -259,12 +259,12 @@ export default function StampPreview({ parameters, onParameterChange, file, show
const scaleY = containerSize.height / heightPts;
const newHeightPx = Math.max(1, drag.initHeight + (y - drag.startY));
const newHeightPts = newHeightPx / scaleY;
onParameterChange('fontSize', newHeightPts as any);
onParameterChange('fontSize', newHeightPts);
}
if (drag.type === 'rotate') {
const angle = Math.atan2(y - drag.centerY, x - drag.centerX) * (180 / Math.PI);
onParameterChange('rotation', angle as any);
onParameterChange('rotation', angle);
}
};
@ -346,9 +346,9 @@ export default function StampPreview({ parameters, onParameterChange, file, show
className={`${styles.gridTile} ${selected || hoverTile === idx ? styles.gridTileSelected : ''} ${hoverTile === idx ? styles.gridTileHovered : ''}`}
onClick={() => {
// Clear overrides to use grid positioning and set position
onParameterChange('overrideX', -1 as any);
onParameterChange('overrideY', -1 as any);
onParameterChange('position', idx as any);
onParameterChange('overrideX', -1);
onParameterChange('overrideY', -1);
onParameterChange('position', idx);
}}
onMouseEnter={() => setHoverTile(idx)}
onMouseLeave={() => setHoverTile(null)}

View File

@ -51,8 +51,8 @@ const StampSetupSettings = ({ parameters, onParameterChange, disabled = false }:
<Select
label={t('AddStampRequest.alphabet', 'Alphabet')}
value={parameters.alphabet}
onChange={(v) => {
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);

View File

@ -14,11 +14,10 @@ export default function AdjustContrastBasicSettings({ parameters, onParameterCha
return (
<Stack gap="md">
<SliderWithInput label={t('adjustContrast.contrast', 'Contrast')} value={parameters.contrast} onChange={(v) => onParameterChange('contrast', v as any)} disabled={disabled} />
<SliderWithInput label={t('adjustContrast.brightness', 'Brightness')} value={parameters.brightness} onChange={(v) => onParameterChange('brightness', v as any)} disabled={disabled} />
<SliderWithInput label={t('adjustContrast.saturation', 'Saturation')} value={parameters.saturation} onChange={(v) => onParameterChange('saturation', v as any)} disabled={disabled} />
<SliderWithInput label={t('adjustContrast.contrast', 'Contrast')} value={parameters.contrast} onChange={(value) => onParameterChange('contrast', value)} disabled={disabled} />
<SliderWithInput label={t('adjustContrast.brightness', 'Brightness')} value={parameters.brightness} onChange={(value) => onParameterChange('brightness', value)} disabled={disabled} />
<SliderWithInput label={t('adjustContrast.saturation', 'Saturation')} value={parameters.saturation} onChange={(value) => onParameterChange('saturation', value)} disabled={disabled} />
</Stack>
);
}

View File

@ -14,11 +14,10 @@ export default function AdjustContrastColorSettings({ parameters, onParameterCha
return (
<Stack gap="md">
<SliderWithInput label={t('adjustContrast.red', 'Red')} value={parameters.red} onChange={(v) => onParameterChange('red', v as any)} disabled={disabled} />
<SliderWithInput label={t('adjustContrast.green', 'Green')} value={parameters.green} onChange={(v) => onParameterChange('green', v as any)} disabled={disabled} />
<SliderWithInput label={t('adjustContrast.blue', 'Blue')} value={parameters.blue} onChange={(v) => onParameterChange('blue', v as any)} disabled={disabled} />
<SliderWithInput label={t('adjustContrast.red', 'Red')} value={parameters.red} onChange={(value) => onParameterChange('red', value)} disabled={disabled} />
<SliderWithInput label={t('adjustContrast.green', 'Green')} value={parameters.green} onChange={(value) => onParameterChange('green', value)} disabled={disabled} />
<SliderWithInput label={t('adjustContrast.blue', 'Blue')} value={parameters.blue} onChange={(value) => onParameterChange('blue', value)} disabled={disabled} />
</Stack>
);
}

View File

@ -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<string, any>) => {
const handleToolConfigSave = (parameters: AutomationParameters) => {
if (configuraingToolIndex >= 0) {
updateTool(configuraingToolIndex, {
configured: true,

View File

@ -16,7 +16,7 @@ interface AutomationEntryProps {
/** Optional description for tooltip */
description?: string;
/** MUI Icon component for the badge */
badgeIcon?: React.ComponentType<any>;
badgeIcon?: React.ComponentType;
/** Array of tool operation names in the workflow */
operations: string[];
/** Click handler */

View File

@ -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<string, any>;
getToolDefaultParameters: (operation: string) => AutomationParameters;
}
export default function ToolList({

View File

@ -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<SubcategoryGroup[]>(() => {
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]
);

View File

@ -6,7 +6,7 @@ import ButtonSelector from "@app/components/shared/ButtonSelector";
interface BookletImpositionSettingsProps {
parameters: BookletImpositionParameters;
onParameterChange: (key: keyof BookletImpositionParameters, value: any) => void;
onParameterChange: <K extends keyof BookletImpositionParameters>(key: K, value: BookletImpositionParameters[K]) => void;
disabled?: boolean;
}
@ -136,7 +136,7 @@ const BookletImpositionSettings = ({ parameters, onParameterChange, disabled = f
<NumberInput
label={t('bookletImposition.gutterSize.label', 'Gutter size (points)')}
value={parameters.gutterSize}
onChange={(value) => onParameterChange('gutterSize', value || 12)}
onChange={(value) => onParameterChange('gutterSize', typeof value === 'number' ? value : parameters.gutterSize)}
min={6}
max={72}
step={6}

View File

@ -5,7 +5,7 @@ import FileUploadButton from "@app/components/shared/FileUploadButton";
interface CertificateFilesSettingsProps {
parameters: CertSignParameters;
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
onParameterChange: <K extends keyof CertSignParameters>(key: K, value: CertSignParameters[K]) => void;
disabled?: boolean;
}
@ -92,4 +92,4 @@ const CertificateFilesSettings = ({ parameters, onParameterChange, disabled = fa
);
};
export default CertificateFilesSettings;
export default CertificateFilesSettings;

View File

@ -3,7 +3,7 @@ import { CertSignParameters } from "@app/hooks/tools/certSign/useCertSignParamet
interface CertificateFormatSettingsProps {
parameters: CertSignParameters;
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
onParameterChange: <K extends keyof CertSignParameters>(key: K, value: CertSignParameters[K]) => void;
disabled?: boolean;
}
@ -67,4 +67,4 @@ const CertificateFormatSettings = ({ parameters, onParameterChange, disabled = f
);
};
export default CertificateFormatSettings;
export default CertificateFormatSettings;

View File

@ -4,7 +4,7 @@ import { useAppConfig } from "@app/contexts/AppConfigContext";
interface CertificateTypeSettingsProps {
parameters: CertSignParameters;
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
onParameterChange: <K extends keyof CertSignParameters>(key: K, value: CertSignParameters[K]) => void;
disabled?: boolean;
}

View File

@ -4,7 +4,7 @@ import { CertSignParameters } from "@app/hooks/tools/certSign/useCertSignParamet
interface SignatureAppearanceSettingsProps {
parameters: CertSignParameters;
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
onParameterChange: <K extends keyof CertSignParameters>(key: K, value: CertSignParameters[K]) => void;
disabled?: boolean;
}
@ -68,7 +68,7 @@ const SignatureAppearanceSettings = ({ parameters, onParameterChange, disabled =
<NumberInput
label={t('certSign.pageNumber', 'Page Number')}
value={parameters.pageNumber}
onChange={(value) => 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;
export default SignatureAppearanceSettings;

View File

@ -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;
export default ExtractImagesSettings;

View File

@ -42,7 +42,7 @@ export interface ExecuteButtonConfig {
export interface ReviewStepConfig {
isVisible: boolean;
operation: ToolOperationHook<any>;
operation: ToolOperationHook<unknown>;
title: string;
onFileClick?: (file: File) => void;
onUndo: () => void;

View File

@ -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

View File

@ -290,8 +290,8 @@ const EmbedPdfViewerContent = ({
file={effectiveFile.file}
url={effectiveFile.url}
enableAnnotations={shouldEnableAnnotations}
signatureApiRef={signatureApiRef as React.RefObject<any>}
historyApiRef={historyApiRef as React.RefObject<any>}
signatureApiRef={signatureApiRef}
historyApiRef={historyApiRef}
onSignatureAdded={() => {
// Handle signature added - for debugging, enable console logs as needed
// Future: Handle signature completion

View File

@ -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<HistoryAPI>(function HistoryAPIBridge(_, ref) {
const { provides: historyApi } = useHistoryCapability();
@ -14,14 +16,15 @@ export const HistoryAPIBridge = forwardRef<HistoryAPI>(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<HistoryAPI>(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<HistoryAPI>(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<HistoryAPI>(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);

View File

@ -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<SignatureAPI>;
historyApiRef?: React.RefObject<HistoryAPI>;
onSignatureAdded?: (annotation: PdfAnnotationObject) => void;
signatureApiRef?: React.RefObject<SignatureAPI | null>;
historyApiRef?: React.RefObject<HistoryAPI | null>;
}
type AnnotationRecord = {
id: string;
pageIndex: number;
rect: PdfAnnotationObject['rect'];
};
export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) {
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: any}>>([]);
const [, setAnnotations] = useState<AnnotationRecord[]>([]);
// 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
})));
}

View File

@ -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]);
}
});

View File

@ -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();

View File

@ -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<SignatureAPI>(function SignatureAPIBridge(_, ref) {
const { provides: annotationApi } = useAnnotationCapability();
const { signatureConfig, storeImageData, isPlacementMode } = useSignature();
@ -16,21 +27,21 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(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<SignatureAPI>(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<SignatureAPI>(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<SignatureAPI>(function SignatureAPI
annotationApi.setActiveTool(null);
},
getPageAnnotations: async (pageIndex: number): Promise<any[]> => {
getPageAnnotations: async (pageIndex: number): Promise<PdfAnnotationObject[]> => {
if (!annotationApi || !annotationApi.getPageAnnotations) {
console.warn('getPageAnnotations not available');
return [];

View File

@ -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,

View File

@ -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<any[]>;
getPageAnnotations: (pageIndex: number) => Promise<PdfAnnotationObject[]>;
}
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;
}

View File

@ -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<any>;
search: (query: string) => Promise<void>;
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<ViewerProviderProps> = ({ 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;
}

View File

@ -479,7 +479,7 @@ export async function undoConsumeFiles(
outputFileIds: FileId[],
filesRef: React.MutableRefObject<Map<FileId, File>>,
dispatch: React.Dispatch<FileContextAction>,
indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any>; deleteFile: (fileId: FileId) => Promise<void> } | null
indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<StirlingFileStub>; deleteFile: (fileId: FileId) => Promise<void> } | null
): Promise<void> {
if (DEBUG) console.log(`📄 undoConsumeFiles: Restoring ${inputStirlingFileStubs.length} input files, removing ${outputFileIds.length} output files`);

View File

@ -52,9 +52,9 @@ export type ToolRegistryEntry = {
// Workbench type for navigation
workbench?: WorkbenchType;
// Operation configuration for automation
operationConfig?: ToolOperationConfig<any>;
operationConfig?: ToolOperationConfig<unknown>;
// Settings component for automation configuration
automationSettings: React.ComponentType<any> | null;
automationSettings: React.ComponentType<unknown> | null;
// Whether this tool supports automation (defaults to true)
supportsAutomate?: boolean;
// Synonyms for search (optional)

View File

@ -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 = <TProps,>(component: ComponentType<TProps>): ComponentType<unknown> =>
component as unknown as ComponentType<unknown>;
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: <LocalIcon icon="signature-rounded" width="1.5rem" height="1.5rem" />,
@ -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: <LocalIcon icon="cleaning-services-outline-rounded" width="1.5rem" height="1.5rem" />,
@ -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: <LocalIcon icon="rotate-right-rounded" width="1.5rem" height="1.5rem" />,
@ -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: <LocalIcon icon="scan-delete-rounded" width="1.5rem" height="1.5rem" />,
@ -521,7 +524,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
endpoints: ["remove-blanks"],
synonyms: getSynonyms(t, "removeBlanks"),
operationConfig: removeBlanksOperationConfig,
automationSettings: RemoveBlanksSettings,
automationSettings: toAutomationSettings(RemoveBlanksSettings),
},
removeAnnotations: {
icon: <LocalIcon icon="thread-unread-rounded" width="1.5rem" height="1.5rem" />,
@ -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: <LocalIcon icon="format-color-fill-rounded" width="1.5rem" height="1.5rem" />,
@ -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")
},
};

View File

@ -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<AutomationTool[]>([]);
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<string, any> => {
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]);

View File

@ -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.'))
});
};
};

View File

@ -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);

View File

@ -16,6 +16,10 @@ import { ToolId } from '@app/types/toolId';
// Re-export for backwards compatibility
export type { ProcessingProgress, ResponseHandler };
type BivariantHandler<TArgs extends unknown[], TResult> = {
bivarianceHack(...args: TArgs): TResult;
}['bivarianceHack'];
export enum ToolType {
singleFile,
multiFile,
@ -62,10 +66,10 @@ export interface SingleFileToolOperationConfig<TParams> 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<TParams> 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<TParams> 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<File[]>;
customProcessor: BivariantHandler<[TParams, File[]], Promise<File[]>>;
}
export type ToolOperationConfig<TParams = void> = SingleFileToolOperationConfig<TParams> | MultiFileToolOperationConfig<TParams> | CustomToolOperationConfig<TParams>;
@ -119,11 +123,11 @@ export interface ToolOperationHook<TParams = void> {
progress: ProcessingProgress | null;
// Actions
executeOperation: (params: TParams, selectedFiles: StirlingFile[]) => Promise<void>;
resetResults: () => void;
clearError: () => void;
cancelOperation: () => void;
undoOperation: () => Promise<void>;
executeOperation(params: TParams, selectedFiles: StirlingFile[]): Promise<void>;
resetResults(): void;
clearError(): void;
cancelOperation(): void;
undoOperation(): Promise<void>;
}
// Re-export for backwards compatibility

View File

@ -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<string, unknown>;
export type SettingsRecord = Record<string, unknown>;
type CombinedSettings<T extends object> = T & SettingsRecord;
type PendingSettings<T extends object> = SettingsWithPending<CombinedSettings<T>> & CombinedSettings<T>;

View File

@ -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;

View File

@ -14,7 +14,7 @@ import TextFieldsIcon from '@mui/icons-material/TextFields';
export interface SuggestedTool {
id: ToolId;
title: string;
icon: React.ComponentType<any>;
icon: React.ComponentType;
href: string;
onClick: (e: React.MouseEvent) => void;
}

View File

@ -268,7 +268,7 @@ export default function HomePage({ openedFile }: HomePageProps = {}) {
<span className="mobile-bottom-button-label">{t('quickAccess.config', 'Config')}</span>
</button>
</div>
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
<FileManager selectedTool={selectedTool} />
<AppConfigModal
opened={configModalOpen}
onClose={() => setConfigModalOpen(false)}
@ -285,7 +285,7 @@ export default function HomePage({ openedFile }: HomePageProps = {}) {
<ToolPanel />
<Workbench />
<RightRail />
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
<FileManager selectedTool={selectedTool} />
</Group>
)}
</div>

View File

@ -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);

View File

@ -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

View File

@ -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<string, number>;
}
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<any> {
async getTeamDetails(teamId: number): Promise<TeamDetailsData> {
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<void> {
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<void> {
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);
},
};

View File

@ -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<T extends object> = <K extends keyof T>(
parameter: K,
value: T[K]
) => void;

View File

@ -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<any>;
tool: () => () => ToolOperationHook<unknown>;
/**
* Static method that returns the default parameters for this tool.
* This enables automation creation to initialize tools with proper defaults.
*/
getDefaultParameters: () => any;
getDefaultParameters: () => Record<string, unknown>;
}
/**
@ -54,7 +54,7 @@ export interface ToolResult {
files?: File[];
error?: string;
downloadUrl?: string;
metadata?: Record<string, any>;
metadata?: Record<string, unknown>;
}
export interface ToolConfiguration {
@ -77,4 +77,3 @@ export interface Tool {
}
export type ToolRegistry = Record<string, Tool>;

View File

@ -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
});
});
});
});

View File

@ -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<SignatureFlatteningResult | null> {
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');

View File

@ -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 {

View File

@ -16,7 +16,7 @@ export interface User {
enabled?: boolean;
is_anonymous?: boolean;
isFirstLogin?: boolean;
app_metadata?: Record<string, any>;
app_metadata?: Record<string, unknown>;
}
export interface Session {
@ -217,7 +217,7 @@ class SpringAuthClient {
*/
async signInWithOAuth(params: {
provider: 'github' | 'google' | 'apple' | 'azure';
options?: { redirectTo?: string; queryParams?: Record<string, any> };
options?: { redirectTo?: string; queryParams?: Record<string, unknown> };
}): Promise<{ error: AuthError | null }> {
try {
// Redirect to Spring OAuth2 endpoint (Vite will proxy to backend)

View File

@ -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 (

View File

@ -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<OAuthProviderConfig> = [
{ 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) => (
<div key={p.id} title={`${t('login.signInWith', 'Sign in with')} ${p.label}`}>
<button
onClick={() => onProviderClick(p.id as any)}
onClick={() => onProviderClick(p.id)}
disabled={isSubmitting}
className="oauth-button-icon"
aria-label={`${t('login.signInWith', 'Sign in with')} ${p.label}`}
@ -46,7 +55,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
{enabledProviders.map((p) => (
<div key={p.id} title={`${t('login.signInWith', 'Sign in with')} ${p.label}`}>
<button
onClick={() => onProviderClick(p.id as any)}
onClick={() => onProviderClick(p.id)}
disabled={isSubmitting}
className="oauth-button-grid"
aria-label={`${t('login.signInWith', 'Sign in with')} ${p.label}`}
@ -64,7 +73,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
{enabledProviders.map((p) => (
<button
key={p.id}
onClick={() => onProviderClick(p.id as any)}
onClick={() => onProviderClick(p.id)}
disabled={isSubmitting}
className="oauth-button-vertical"
title={p.label}