mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-01 20:10:35 +01:00
Fix more any type uses
This commit is contained in:
parent
373f16a86f
commit
614da8d6a8
@ -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 }) => {
|
||||
|
||||
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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' },
|
||||
|
||||
@ -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={{
|
||||
|
||||
@ -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') },
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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]
|
||||
);
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
})));
|
||||
}
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 [];
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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`);
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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")
|
||||
},
|
||||
};
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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.'))
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>;
|
||||
|
||||
|
||||
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user