mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
Refactor refs to use RefObject and improve type safety
Updated various components, hooks, and context files to use React.RefObject instead of React.Ref for improved type safety and consistency. Enhanced type definitions for automation parameters, fixed minor bugs, and improved error handling in tool operations and automation execution. Also updated utility functions and test files to align with these changes.
This commit is contained in:
parent
ac5c4043db
commit
c3b604791a
@ -14,7 +14,7 @@ interface DragDropGridProps<T extends DragDropItem> {
|
||||
selectionMode: boolean;
|
||||
isAnimating: boolean;
|
||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
||||
renderItem: (item: T, index: number, refs: React.Ref<Map<string, HTMLDivElement>>) => React.ReactNode;
|
||||
renderItem: (item: T, index: number, refs: React.RefObject<Map<string, HTMLDivElement>>) => React.ReactNode;
|
||||
renderSplitMarker?: (item: T, index: number) => React.ReactNode;
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ interface PageThumbnailProps {
|
||||
selectionMode: boolean;
|
||||
movingPage: number | null;
|
||||
isAnimating: boolean;
|
||||
pageRefs: React.Ref<Map<string, HTMLDivElement>>;
|
||||
pageRefs: React.RefObject<Map<string, HTMLDivElement>>;
|
||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
||||
onTogglePage: (pageId: string) => void;
|
||||
onAnimateReorder: () => void;
|
||||
@ -65,7 +65,11 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||
const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null);
|
||||
const dragElementRef = useRef<HTMLDivElement & { __dragCleanup?: () => void } | null>(null);
|
||||
interface DragElement extends HTMLDivElement {
|
||||
__dragCleanup?: () => void;
|
||||
}
|
||||
|
||||
const dragElementRef = useRef<DragElement | null>(null);
|
||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
||||
const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
@ -171,19 +175,15 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
type: 'page',
|
||||
pageNumber: page.pageNumber
|
||||
}),
|
||||
onDrop: (_) => {}
|
||||
onDrop: (_) => { /* empty */ }
|
||||
});
|
||||
|
||||
dragElementRef.current.__dragCleanup = () => {
|
||||
dragCleanup();
|
||||
dropCleanup();
|
||||
};
|
||||
} else {
|
||||
if (pageRefs && 'current' in pageRefs && pageRefs.current) {
|
||||
pageRefs.current.delete(page.id);
|
||||
}
|
||||
if (dragElementRef.current && (dragElementRef.current as any).__dragCleanup) {
|
||||
dragElementRef.current.__dragCleanup?.();
|
||||
if (dragElementRef.current?.__dragCleanup) {
|
||||
dragElementRef.current.__dragCleanup();
|
||||
}
|
||||
}
|
||||
}, [page.id, page.pageNumber, pageRefs, selectionMode, selectedPageIds, pdfDocument.pages, onReorderPages]);
|
||||
|
@ -11,6 +11,24 @@ import { ChangeMetadataParameters } from 'src/hooks/tools/changeMetadata/useChan
|
||||
import { OCRParameters } from 'src/hooks/tools/ocr/useOCRParameters';
|
||||
import { TooltipTip } from 'src/types/tips';
|
||||
import { RemovePasswordParameters } from 'src/hooks/tools/removePassword/useRemovePasswordParameters';
|
||||
import { SanitizeParameters } from 'src/hooks/tools/sanitize/useSanitizeParameters';
|
||||
import { RotateParameters } from 'src/hooks/tools/rotate/useRotateParameters';
|
||||
import { RemovePagesParameters } from 'src/hooks/tools/removePages/useRemovePagesParameters';
|
||||
import { RemoveBlanksParameters } from 'src/hooks/tools/removeBlanks/useRemoveBlanksParameters';
|
||||
import { RedactParameters } from 'src/hooks/tools/redact/useRedactParameters';
|
||||
import { MergeParameters } from 'src/hooks/tools/merge/useMergeParameters';
|
||||
import { CropParameters } from 'src/hooks/tools/crop/useCropParameters';
|
||||
import { ConvertParameters } from 'src/hooks/tools/convert/useConvertParameters';
|
||||
import { ChangePermissionsParameters } from 'src/hooks/tools/changePermissions/useChangePermissionsParameters';
|
||||
import { CertSignParameters } from 'src/hooks/tools/certSign/useCertSignParameters';
|
||||
import { BookletImpositionParameters } from 'src/hooks/tools/bookletImposition/useBookletImpositionParameters';
|
||||
import { AutoRenameParameters } from 'src/hooks/tools/autoRename/useAutoRenameParameters';
|
||||
import { FlattenParameters } from 'src/hooks/tools/flatten/useFlattenParameters';
|
||||
import { AutomateParameters } from 'src/types/automation';
|
||||
import { AdjustPageScaleParameters } from 'src/hooks/tools/adjustPageScale/useAdjustPageScaleParameters';
|
||||
import { AddWatermarkParameters } from 'src/hooks/tools/addWatermark/useAddWatermarkParameters';
|
||||
import { AddStampParameters } from '../addStamp/useAddStampParameters';
|
||||
import { AddPasswordFullParameters } from 'src/hooks/tools/addPassword/useAddPasswordParameters';
|
||||
|
||||
export interface FilesStepConfig {
|
||||
selectedFiles: StirlingFile[];
|
||||
@ -47,7 +65,29 @@ export interface ExecuteButtonConfig {
|
||||
|
||||
export interface ReviewStepConfig {
|
||||
isVisible: boolean;
|
||||
operation: ToolOperationHook<SplitParameters> | ToolOperationHook<CompressParameters> | ToolOperationHook<ChangeMetadataParameters> | ToolOperationHook<OCRParameters> | ToolOperationHook<RemovePasswordParameters>;
|
||||
operation: ToolOperationHook<SplitParameters> |
|
||||
ToolOperationHook<CompressParameters> |
|
||||
ToolOperationHook<ChangeMetadataParameters> |
|
||||
ToolOperationHook<OCRParameters> |
|
||||
ToolOperationHook<RemovePasswordParameters> |
|
||||
ToolOperationHook<SanitizeParameters> |
|
||||
ToolOperationHook<RotateParameters> |
|
||||
ToolOperationHook<RemovePagesParameters> |
|
||||
ToolOperationHook<RemoveBlanksParameters> |
|
||||
ToolOperationHook<RedactParameters> |
|
||||
ToolOperationHook<MergeParameters> |
|
||||
ToolOperationHook<ConvertParameters> |
|
||||
ToolOperationHook<ChangePermissionsParameters> |
|
||||
ToolOperationHook<CertSignParameters> |
|
||||
ToolOperationHook<BookletImpositionParameters> |
|
||||
ToolOperationHook<AutoRenameParameters> |
|
||||
ToolOperationHook<FlattenParameters> |
|
||||
ToolOperationHook<AutomateParameters> |
|
||||
ToolOperationHook<AdjustPageScaleParameters> |
|
||||
ToolOperationHook<AddWatermarkParameters> |
|
||||
ToolOperationHook<AddStampParameters> |
|
||||
ToolOperationHook<AddPasswordFullParameters> |
|
||||
ToolOperationHook<CropParameters>;
|
||||
title: string;
|
||||
onFileClick?: (file: File) => void;
|
||||
onUndo: () => void;
|
||||
@ -99,7 +139,11 @@ export function createToolFlow(config: ToolFlowConfig) {
|
||||
{/* Execute Button */}
|
||||
{config.executeButton && config.executeButton.isVisible !== false && (
|
||||
<OperationButton
|
||||
onClick={config.executeButton.onClick}
|
||||
onClick={() => {
|
||||
if (config.executeButton) {
|
||||
config.executeButton.onClick().catch(console.error);
|
||||
}
|
||||
}}
|
||||
isLoading={config.review.operation.isLoading}
|
||||
disabled={config.executeButton.disabled}
|
||||
loadingText={config.executeButton.loadingText}
|
||||
@ -109,9 +153,9 @@ export function createToolFlow(config: ToolFlowConfig) {
|
||||
)}
|
||||
|
||||
{/* Review Step */}
|
||||
{steps.createReviewStep({
|
||||
{steps.createReviewStep<SplitParameters | CompressParameters | ChangeMetadataParameters | OCRParameters | RemovePasswordParameters>({
|
||||
isVisible: config.review.isVisible,
|
||||
operation: config.review.operation,
|
||||
operation: config.review.operation as ToolOperationHook<SplitParameters | CompressParameters | ChangeMetadataParameters | OCRParameters | RemovePasswordParameters>,
|
||||
title: config.review.title,
|
||||
onFileClick: config.review.onFileClick,
|
||||
onUndo: config.review.onUndo
|
||||
|
@ -165,8 +165,8 @@ interface AddFileOptions {
|
||||
*/
|
||||
export async function addFiles(
|
||||
options: AddFileOptions,
|
||||
stateRef: React.Ref<FileContextState>,
|
||||
filesRef: React.Ref<Map<FileId, File>>,
|
||||
stateRef: React.RefObject<FileContextState>,
|
||||
filesRef: React.RefObject<Map<FileId, File>>,
|
||||
dispatch: React.Dispatch<FileContextAction>,
|
||||
lifecycleManager: FileLifecycleManager,
|
||||
enablePersistence = false
|
||||
@ -204,7 +204,7 @@ export async function addFiles(
|
||||
let thumbnail: string | undefined;
|
||||
if (processedFileMetadata) {
|
||||
// PDF file - use thumbnail from processedFile metadata
|
||||
thumbnail = processedFileMetadata.thumbnailUrl;
|
||||
thumbnail = processedFileMetadata.thumbnailUrl as string | undefined;
|
||||
if (DEBUG) console.log(`📄 Generated PDF metadata for ${file.name}: ${processedFileMetadata.totalPages} pages, thumbnail: SUCCESS`);
|
||||
} else if (!file.type.startsWith('application/pdf')) {
|
||||
// Non-PDF files: simple thumbnail generation, no processedFile metadata
|
||||
@ -278,7 +278,7 @@ export async function consumeFiles(
|
||||
inputFileIds: FileId[],
|
||||
outputStirlingFiles: StirlingFile[],
|
||||
outputStirlingFileStubs: StirlingFileStub[],
|
||||
filesRef: React.Ref<Map<FileId, File>>,
|
||||
filesRef: React.RefObject<Map<FileId, File>>,
|
||||
dispatch: React.Dispatch<FileContextAction>
|
||||
): Promise<FileId[]> {
|
||||
if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputStirlingFiles.length} output files with pre-created stubs`);
|
||||
@ -357,7 +357,7 @@ export async function consumeFiles(
|
||||
async function restoreFilesAndCleanup(
|
||||
filesToRestore: { file: File; record: StirlingFileStub }[],
|
||||
fileIdsToRemove: FileId[],
|
||||
filesRef: React.Ref<Map<FileId, File>>,
|
||||
filesRef: React.RefObject<Map<FileId, File>>,
|
||||
indexedDB?: { deleteFile: (fileId: FileId) => Promise<void> } | null
|
||||
): Promise<void> {
|
||||
// Remove files from filesRef
|
||||
@ -406,7 +406,7 @@ export async function undoConsumeFiles(
|
||||
inputFiles: File[],
|
||||
inputStirlingFileStubs: StirlingFileStub[],
|
||||
outputFileIds: FileId[],
|
||||
filesRef: React.Ref<Map<FileId, File>>,
|
||||
filesRef: React.RefObject<Map<FileId, File>>,
|
||||
dispatch: React.Dispatch<FileContextAction>,
|
||||
indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any>; deleteFile: (fileId: FileId) => Promise<void> } | null
|
||||
): Promise<void> {
|
||||
@ -468,8 +468,8 @@ export async function undoConsumeFiles(
|
||||
export async function addStirlingFileStubs(
|
||||
stirlingFileStubs: StirlingFileStub[],
|
||||
options: { insertAfterPageId?: string; selectFiles?: boolean } = {},
|
||||
stateRef: React.Ref<FileContextState>,
|
||||
filesRef: React.Ref<Map<FileId, File>>,
|
||||
stateRef: React.RefObject<FileContextState>,
|
||||
filesRef: React.RefObject<Map<FileId, File>>,
|
||||
dispatch: React.Dispatch<FileContextAction>,
|
||||
_lifecycleManager: FileLifecycleManager
|
||||
): Promise<StirlingFile[]> {
|
||||
|
@ -15,8 +15,8 @@ import {
|
||||
* Create stable selectors using stateRef and filesRef
|
||||
*/
|
||||
export function createFileSelectors(
|
||||
stateRef: React.Ref<FileContextState>,
|
||||
filesRef: React.Ref<Map<FileId, File>>
|
||||
stateRef: React.RefObject<FileContextState>,
|
||||
filesRef: React.RefObject<Map<FileId, File>>
|
||||
): FileContextSelectors {
|
||||
return {
|
||||
getFile: (id: FileId) => {
|
||||
@ -125,8 +125,8 @@ export function buildQuickKeySetFromMetadata(metadata: { name: string; size: num
|
||||
* Get primary file (first in list) - commonly used pattern
|
||||
*/
|
||||
export function getPrimaryFile(
|
||||
stateRef: React.Ref<FileContextState>,
|
||||
filesRef: React.Ref<Map<FileId, File>>
|
||||
stateRef: React.RefObject<FileContextState>,
|
||||
filesRef: React.RefObject<Map<FileId, File>>
|
||||
): { file?: File; record?: StirlingFileStub } {
|
||||
const primaryFileId = stateRef.current.files.ids[0];
|
||||
if (!primaryFileId) return {};
|
||||
|
@ -16,7 +16,7 @@ export class FileLifecycleManager {
|
||||
private fileGenerations = new Map<string, number>(); // Generation tokens to prevent stale cleanup
|
||||
|
||||
constructor(
|
||||
private filesRef: React.Ref<Map<FileId, File>>,
|
||||
private filesRef: React.RefObject<Map<FileId, File> | null>,
|
||||
private dispatch: React.Dispatch<FileContextAction>
|
||||
) {}
|
||||
|
||||
@ -34,7 +34,7 @@ export class FileLifecycleManager {
|
||||
/**
|
||||
* Clean up resources for a specific file (with stateRef access for complete cleanup)
|
||||
*/
|
||||
cleanupFile = (fileId: FileId, stateRef?: React.Ref<any>): void => {
|
||||
cleanupFile = (fileId: FileId, stateRef?: React.RefObject<any>): void => {
|
||||
// Use comprehensive cleanup (same as removeFiles)
|
||||
this.cleanupAllResourcesForFile(fileId, stateRef);
|
||||
|
||||
@ -62,13 +62,13 @@ export class FileLifecycleManager {
|
||||
this.fileGenerations.clear();
|
||||
|
||||
// Clear files ref
|
||||
this.filesRef.current.clear();
|
||||
this.filesRef.current?.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* Schedule delayed cleanup for a file with generation token to prevent stale cleanup
|
||||
*/
|
||||
scheduleCleanup = (fileId: FileId, delay = 30000, stateRef?: React.Ref<any>): void => {
|
||||
scheduleCleanup = (fileId: FileId, delay = 30000, stateRef?: React.RefObject<any>): void => {
|
||||
// Cancel existing timer
|
||||
const existingTimer = this.cleanupTimers.get(fileId);
|
||||
if (existingTimer) {
|
||||
@ -101,7 +101,7 @@ export class FileLifecycleManager {
|
||||
/**
|
||||
* Remove a file immediately with complete resource cleanup
|
||||
*/
|
||||
removeFiles = (fileIds: FileId[], stateRef?: React.Ref<any>): void => {
|
||||
removeFiles = (fileIds: FileId[], stateRef?: React.RefObject<any>): void => {
|
||||
fileIds.forEach(fileId => {
|
||||
// Clean up all resources for this file
|
||||
this.cleanupAllResourcesForFile(fileId, stateRef);
|
||||
@ -114,9 +114,9 @@ export class FileLifecycleManager {
|
||||
/**
|
||||
* Complete resource cleanup for a single file
|
||||
*/
|
||||
private cleanupAllResourcesForFile = (fileId: FileId, stateRef?: React.Ref<any>): void => {
|
||||
private cleanupAllResourcesForFile = (fileId: FileId, stateRef?: React.RefObject<any>): void => {
|
||||
// Remove from files ref
|
||||
this.filesRef.current.delete(fileId);
|
||||
this.filesRef.current?.delete(fileId);
|
||||
|
||||
// Cancel cleanup timer and generation
|
||||
const timer = this.cleanupTimers.get(fileId);
|
||||
@ -166,15 +166,15 @@ export class FileLifecycleManager {
|
||||
/**
|
||||
* Update file record with race condition guards
|
||||
*/
|
||||
updateStirlingFileStub = (fileId: FileId, updates: Partial<StirlingFileStub>, stateRef?: React.Ref<any>): void => {
|
||||
updateStirlingFileStub = (fileId: FileId, updates: Partial<StirlingFileStub>, stateRef?: React.RefObject<any>): void => {
|
||||
// Guard against updating removed files (race condition protection)
|
||||
if (!this.filesRef.current.has(fileId)) {
|
||||
if (!this.filesRef.current?.has(fileId)) {
|
||||
if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Additional state guard for rare race conditions
|
||||
if (stateRef && !stateRef.current.files.byId[fileId]) {
|
||||
if (stateRef && 'current' in stateRef && stateRef.current && !stateRef.current.files.byId[fileId]) {
|
||||
if (DEBUG) console.warn(`🗂️ Attempted to update removed file (state): ${fileId}`);
|
||||
return;
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ export function useSuggestedAutomations(): SuggestedAutomation[] {
|
||||
{
|
||||
operation: "ocr",
|
||||
parameters: {
|
||||
languages: ['eng'],
|
||||
languages: ['eng'] as (string | null)[],
|
||||
ocrType: 'skip-text',
|
||||
ocrRenderType: 'hocr',
|
||||
additionalOptions: ['clean', 'cleanFinal'],
|
||||
@ -175,7 +175,7 @@ export function useSuggestedAutomations(): SuggestedAutomation[] {
|
||||
{
|
||||
operation: "ocr",
|
||||
parameters: {
|
||||
languages: ['eng'],
|
||||
languages: ['eng'] as (string | null)[],
|
||||
ocrType: 'skip-text',
|
||||
ocrRenderType: 'hocr',
|
||||
additionalOptions: [],
|
||||
|
@ -44,11 +44,15 @@ export const useToolApiCalls = <TParams = void>() => {
|
||||
|
||||
// Forward to shared response processor (uses tool-specific responseHandler if provided)
|
||||
const responseFiles = await processResponse(
|
||||
response.data,
|
||||
response.data as Blob,
|
||||
[file],
|
||||
config.filePrefix,
|
||||
config.responseHandler,
|
||||
config.preserveBackendFilename ? response.headers : undefined
|
||||
config.preserveBackendFilename
|
||||
? Object.fromEntries(
|
||||
Object.entries(response.headers as Record<string, string | undefined>).map(([key, value]) => [key, value?.toString()])
|
||||
)
|
||||
: undefined
|
||||
);
|
||||
processedFiles.push(...responseFiles);
|
||||
|
||||
|
@ -36,10 +36,10 @@ export const useDocumentMeta = (meta: MetaOptions) => {
|
||||
let metaElement = document.querySelector(`meta[name="${name}"]`)!;
|
||||
if (!metaElement) {
|
||||
metaElement = document.createElement('meta');
|
||||
metaElement.name = name;
|
||||
(metaElement as HTMLMetaElement).name = name;
|
||||
document.head.appendChild(metaElement);
|
||||
}
|
||||
metaElement.content = content;
|
||||
(metaElement as HTMLMetaElement).content = content;
|
||||
};
|
||||
|
||||
const updateOrCreateProperty = (property: string, content: string) => {
|
||||
@ -49,7 +49,7 @@ export const useDocumentMeta = (meta: MetaOptions) => {
|
||||
metaElement.setAttribute('property', property);
|
||||
document.head.appendChild(metaElement);
|
||||
}
|
||||
metaElement.content = content;
|
||||
(metaElement as HTMLMetaElement).content = content;
|
||||
};
|
||||
|
||||
// Update meta tags
|
||||
@ -90,7 +90,7 @@ export const useDocumentMeta = (meta: MetaOptions) => {
|
||||
const element = document.querySelector(`meta[property="${property}"]`)!;
|
||||
if (element) {
|
||||
if (originalValue !== null) {
|
||||
element.content = originalValue;
|
||||
(element as HTMLMetaElement).content = originalValue;
|
||||
} else {
|
||||
element.remove();
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ import * as React from 'react';
|
||||
*/
|
||||
|
||||
|
||||
export const useIsOverflowing = (ref: React.Ref<HTMLElement | null>, callback?: (isOverflow: boolean) => void) => {
|
||||
export const useIsOverflowing = (ref: React.RefObject<HTMLElement | null>, callback?: (isOverflow: boolean) => void) => {
|
||||
// State to track overflow status
|
||||
const [isOverflow, setIsOverflow] = React.useState<boolean | undefined>(undefined);
|
||||
|
||||
|
@ -60,8 +60,8 @@ export function useTooltipPosition({
|
||||
sidebarTooltip: boolean;
|
||||
position: Position;
|
||||
gap: number;
|
||||
triggerRef: React.Ref<HTMLElement | null>;
|
||||
tooltipRef: React.Ref<HTMLDivElement | null>;
|
||||
triggerRef: React.RefObject<HTMLElement | null>;
|
||||
tooltipRef: React.RefObject<HTMLDivElement | null>;
|
||||
sidebarRefs?: SidebarRefs;
|
||||
sidebarState?: SidebarState;
|
||||
}): PositionState {
|
||||
|
@ -173,8 +173,10 @@ class AutomationStorage {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return automations.filter(automation =>
|
||||
automation.name.toLowerCase().includes(lowerQuery) ||
|
||||
(automation.description?.toLowerCase().includes(lowerQuery)) ??
|
||||
automation.operations.some(op => op.operation.toLowerCase().includes(lowerQuery))
|
||||
((automation.description?.toLowerCase().includes(lowerQuery)) ?? false),
|
||||
automations.some(automation =>
|
||||
automation.operations.some(op => op.operation.toLowerCase().includes(lowerQuery))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { useFileSelection } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
import { useToolWorkflow } from "../contexts/ToolWorkflowContext";
|
||||
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
import { createToolFlow, MiddleStepConfig } from "../components/tools/shared/createToolFlow";
|
||||
import { createFilesToolStep } from "../components/tools/shared/FilesToolStep";
|
||||
import AutomationSelection from "../components/tools/automate/AutomationSelection";
|
||||
import AutomationCreation from "../components/tools/automate/AutomationCreation";
|
||||
@ -14,8 +14,9 @@ import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperati
|
||||
import { BaseToolProps } from "../types/tool";
|
||||
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
|
||||
import { useSavedAutomations } from "../hooks/tools/automate/useSavedAutomations";
|
||||
import { AutomationConfig, AutomationStepData, AutomationMode, AutomationStep } from "../types/automation";
|
||||
import { AutomationConfig, AutomationStepData, AutomationMode, AutomationStep, AutomateParameters } from "../types/automation";
|
||||
import { AUTOMATION_STEPS } from "../constants/automation";
|
||||
import { StirlingFile } from "src/types/fileContext";
|
||||
|
||||
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
@ -146,7 +147,13 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
<AutomationRun
|
||||
automation={stepData.automation}
|
||||
onComplete={handleComplete}
|
||||
automateOperation={automateOperation}
|
||||
automateOperation={{
|
||||
...automateOperation,
|
||||
executeOperation: async (params, files) => {
|
||||
const stirlingFiles = files as StirlingFile[]; // Ensure type compatibility
|
||||
await automateOperation.executeOperation(params as AutomateParameters, stirlingFiles);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -155,11 +162,14 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const createStep = (title: string, props: any, content?: React.ReactNode) => ({
|
||||
title,
|
||||
...props,
|
||||
content
|
||||
});
|
||||
const createStep = (title: string, props: Record<string, unknown>, content?: React.ReactNode): React.ReactElement => {
|
||||
return (
|
||||
<div {...props}>
|
||||
<h3>{title}</h3>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Always create files step to avoid conditional hook calls
|
||||
const filesStep = createFilesToolStep(createStep, {
|
||||
@ -167,8 +177,10 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
isCollapsed: hasResults,
|
||||
});
|
||||
|
||||
const automationSteps = [
|
||||
createStep(t('automate.selection.title', 'Automation Selection'), {
|
||||
const automationSteps: MiddleStepConfig[] = [
|
||||
{
|
||||
title: t('automate.selection.title', 'Automation Selection'),
|
||||
content: currentStep === AUTOMATION_STEPS.SELECTION ? renderCurrentStep() : null,
|
||||
isVisible: true,
|
||||
isCollapsed: currentStep !== AUTOMATION_STEPS.SELECTION,
|
||||
onCollapsedClick: () => {
|
||||
@ -177,26 +189,27 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
setCurrentStep(AUTOMATION_STEPS.SELECTION);
|
||||
setStepData({ step: AUTOMATION_STEPS.SELECTION });
|
||||
}
|
||||
}, currentStep === AUTOMATION_STEPS.SELECTION ? renderCurrentStep() : null),
|
||||
|
||||
createStep(stepData.mode === AutomationMode.EDIT
|
||||
? t('automate.creation.editTitle', 'Edit Automation')
|
||||
: t('automate.creation.createTitle', 'Create Automation'), {
|
||||
},
|
||||
{
|
||||
title: stepData.mode === AutomationMode.EDIT
|
||||
? t('automate.creation.editTitle', 'Edit Automation')
|
||||
: t('automate.creation.createTitle', 'Create Automation'),
|
||||
content: currentStep === AUTOMATION_STEPS.CREATION ? renderCurrentStep() : null,
|
||||
isVisible: currentStep === AUTOMATION_STEPS.CREATION,
|
||||
isCollapsed: false
|
||||
}, currentStep === AUTOMATION_STEPS.CREATION ? renderCurrentStep() : null),
|
||||
|
||||
// Files step - only visible during run mode
|
||||
},
|
||||
{
|
||||
...filesStep,
|
||||
title: t('automate.files.title', 'Files'),
|
||||
content: null, // Files step content is managed separately
|
||||
isVisible: currentStep === AUTOMATION_STEPS.RUN
|
||||
},
|
||||
|
||||
// Run step
|
||||
createStep(t('automate.run.title', 'Run Automation'), {
|
||||
{
|
||||
title: t('automate.run.title', 'Run Automation'),
|
||||
content: currentStep === AUTOMATION_STEPS.RUN ? renderCurrentStep() : null,
|
||||
isVisible: currentStep === AUTOMATION_STEPS.RUN,
|
||||
isCollapsed: hasResults,
|
||||
}, currentStep === AUTOMATION_STEPS.RUN ? renderCurrentStep() : null)
|
||||
isCollapsed: hasResults
|
||||
}
|
||||
];
|
||||
|
||||
return createToolFlow({
|
||||
@ -214,7 +227,11 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
onPreviewFile?.(file);
|
||||
actions.setWorkbench('viewer');
|
||||
},
|
||||
onUndo: handleUndo
|
||||
onUndo: () => {
|
||||
handleUndo().catch((error) => {
|
||||
console.error('Undo operation failed:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -54,7 +54,11 @@ const RemovePages = (props: BaseToolProps) => {
|
||||
operation: base.operation,
|
||||
title: t("removePages.results.title", "Pages Removed"),
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
onUndo: () => {
|
||||
base.handleUndo().catch((error) => {
|
||||
console.error("Undo operation failed:", error);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -50,7 +50,11 @@ const Rotate = (props: BaseToolProps) => {
|
||||
operation: base.operation,
|
||||
title: t("rotate.title", "Rotation Results"),
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
onUndo: () => {
|
||||
base.handleUndo().catch((error) => {
|
||||
console.error("Undo operation failed:", error);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
export interface AutomationOperation {
|
||||
operation: string;
|
||||
parameters: Record<string, string | number | boolean | null>;
|
||||
parameters: Record<string, JsonValue>;
|
||||
}
|
||||
|
||||
export interface AutomationConfig {
|
||||
@ -22,7 +22,7 @@ export interface AutomationTool {
|
||||
operation: string;
|
||||
name: string;
|
||||
configured: boolean;
|
||||
parameters?: Record<string, any>;
|
||||
parameters?: Record<string, JsonValue>;
|
||||
}
|
||||
|
||||
export type AutomationStep = typeof import('../constants/automation').AUTOMATION_STEPS[keyof typeof import('../constants/automation').AUTOMATION_STEPS];
|
||||
@ -47,10 +47,6 @@ export interface AutomationExecutionCallbacks {
|
||||
onStepError?: (stepIndex: number, error: string) => void;
|
||||
}
|
||||
|
||||
export interface AutomateParameters extends AutomationExecutionCallbacks {
|
||||
automationConfig?: AutomationConfig;
|
||||
}
|
||||
|
||||
export enum AutomationMode {
|
||||
CREATE = 'create',
|
||||
EDIT = 'edit',
|
||||
@ -71,3 +67,49 @@ export interface SuggestedAutomation {
|
||||
export interface AutomateParameters extends AutomationExecutionCallbacks {
|
||||
automationConfig?: AutomationConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typen für Automations-Funktionalität
|
||||
*/
|
||||
|
||||
// JSON-ähnlicher Wertetyp: erlaubt Strings, Zahlen, Booleans, null,
|
||||
// Arrays und verschachtelte Objekte – genau das, was "parameters" benötigt.
|
||||
export type JsonValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| JsonValue[]
|
||||
| { [key: string]: JsonValue };
|
||||
|
||||
export type JsonObject = Record<string, JsonValue>;
|
||||
|
||||
export interface AutomationOperation {
|
||||
operation: string;
|
||||
// Wurde von Record<string, string | number | boolean | null> auf JSON erweitert
|
||||
parameters: Record<string, JsonValue>;
|
||||
}
|
||||
|
||||
export interface AutomationConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
operations: AutomationOperation[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface AutomationStepData {
|
||||
step: AutomationStep;
|
||||
mode?: AutomationMode;
|
||||
automation?: AutomationConfig;
|
||||
}
|
||||
|
||||
export interface ExecutionStep {
|
||||
id: string;
|
||||
operation: string;
|
||||
name: string;
|
||||
status: 'pending' | 'running' | 'completed' | 'error';
|
||||
error?: string;
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ export interface SidebarState {
|
||||
}
|
||||
|
||||
export interface SidebarRefs {
|
||||
quickAccessRef: React.Ref<HTMLDivElement | null>;
|
||||
toolPanelRef: React.Ref<HTMLDivElement | null>;
|
||||
quickAccessRef: React.RefObject<HTMLDivElement | null>;
|
||||
toolPanelRef: React.RefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
export interface SidebarInfo {
|
||||
|
@ -62,11 +62,15 @@ export const executeToolOperationWithPrefix = async (
|
||||
timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
||||
});
|
||||
|
||||
console.log(`📥 Response status: ${response.status}, size: ${response.data.size} bytes`);
|
||||
if (response.data instanceof Blob) {
|
||||
console.log(`📥 Response status: ${response.status}, size: ${response.data.size} bytes`);
|
||||
} else {
|
||||
console.warn(`📥 Response data is not a Blob, unable to determine size.`);
|
||||
}
|
||||
|
||||
// Multi-file responses are typically ZIP files, but may be single files (e.g. split with merge=true)
|
||||
let result;
|
||||
if (response.data.type === 'application/pdf' ||
|
||||
if ((response.data as Blob).type === 'application/pdf' ||
|
||||
(response.headers && response.headers['content-type'] === 'application/pdf')) {
|
||||
// Single PDF response (e.g. split with merge option) - use processResponse to respect preserveBackendFilename
|
||||
const processedFiles = await processResponse(
|
||||
@ -85,7 +89,11 @@ export const executeToolOperationWithPrefix = async (
|
||||
};
|
||||
} else {
|
||||
// ZIP response
|
||||
result = await AutomationFileProcessor.extractAutomationZipFiles(response.data);
|
||||
if (response.data instanceof Blob) {
|
||||
result = await AutomationFileProcessor.extractAutomationZipFiles(response.data);
|
||||
} else {
|
||||
throw new Error('Response data is not a Blob, unable to process ZIP files.');
|
||||
}
|
||||
}
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
@ -123,15 +131,21 @@ export const executeToolOperationWithPrefix = async (
|
||||
timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
||||
});
|
||||
|
||||
console.log(`📥 Response ${i+1} status: ${response.status}, size: ${response.data.size} bytes`);
|
||||
if (response.data instanceof Blob) {
|
||||
console.log(`📥 Response ${i+1} status: ${response.status}, size: ${response.data.size} bytes`);
|
||||
} else {
|
||||
console.warn(`📥 Response ${i+1} data is not a Blob, unable to determine size.`);
|
||||
}
|
||||
|
||||
// Create result file using processResponse to respect preserveBackendFilename setting
|
||||
const processedFiles = await processResponse(
|
||||
response.data,
|
||||
response.data as Blob,
|
||||
[file],
|
||||
filePrefix,
|
||||
undefined,
|
||||
config.preserveBackendFilename ? response.headers : undefined
|
||||
config.preserveBackendFilename ? Object.fromEntries(
|
||||
Object.entries(response.headers || {}).map(([key, value]) => [key, value != null ? String(value) : undefined])
|
||||
) : undefined
|
||||
);
|
||||
resultFiles.push(...processedFiles);
|
||||
console.log(`✅ Created result file(s): ${processedFiles.map(f => f.name).join(', ')}`);
|
||||
@ -141,17 +155,26 @@ export const executeToolOperationWithPrefix = async (
|
||||
return resultFiles;
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error(`Tool operation ${operationName} failed:`, error);
|
||||
throw new Error(`${operationName} operation failed: ${error.response?.data ?? error.message}`);
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`${operationName} operation failed: ${error.message}`);
|
||||
} else {
|
||||
throw new Error(`${operationName} operation failed: Unknown error`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute an entire automation sequence
|
||||
*/
|
||||
interface Automation {
|
||||
name?: string;
|
||||
operations: { operation: string; parameters?: Record<string, unknown> }[];
|
||||
}
|
||||
|
||||
export const executeAutomationSequence = async (
|
||||
automation: any,
|
||||
automation: Automation,
|
||||
initialFiles: File[],
|
||||
toolRegistry: ToolRegistry,
|
||||
onStepStart?: (stepIndex: number, operationName: string) => void,
|
||||
@ -191,9 +214,9 @@ export const executeAutomationSequence = async (
|
||||
currentFiles = resultFiles;
|
||||
onStepComplete?.(i, resultFiles);
|
||||
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error(`❌ Step ${i + 1} failed:`, error);
|
||||
onStepError?.(i, error.message);
|
||||
onStepError?.(i, error instanceof Error ? error.message : 'Unknown error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ export class FileHasher {
|
||||
offset += chunk.byteLength;
|
||||
}
|
||||
|
||||
return combined.buffer;
|
||||
return Promise.resolve(combined.buffer);
|
||||
}
|
||||
|
||||
private static async hashArrayBuffer(buffer: ArrayBuffer): Promise<string> {
|
||||
|
@ -135,7 +135,7 @@ describe('fileResponseUtils', () => {
|
||||
|
||||
test('should handle null/undefined headers gracefully', () => {
|
||||
const responseData = new Uint8Array([1, 2, 3, 4]);
|
||||
const headers = null;
|
||||
const headers = {} as Record<string, string | undefined>;
|
||||
const fallbackFilename = 'test.bin';
|
||||
|
||||
const file = createFileFromApiResponse(responseData, headers, fallbackFilename);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RefObject } from 'react';
|
||||
import { SidebarRefs, SidebarState, SidebarInfo } from '../types/sidebar';
|
||||
|
||||
/**
|
||||
@ -7,7 +8,10 @@ import { SidebarRefs, SidebarState, SidebarInfo } from '../types/sidebar';
|
||||
* @returns Object containing the sidebar rect and whether the tool panel is active
|
||||
*/
|
||||
export function getSidebarInfo(refs: SidebarRefs, state: SidebarState): SidebarInfo {
|
||||
const { quickAccessRef, toolPanelRef } = refs;
|
||||
const { quickAccessRef, toolPanelRef } = refs as {
|
||||
quickAccessRef: RefObject<HTMLDivElement>;
|
||||
toolPanelRef: RefObject<HTMLDivElement>;
|
||||
};
|
||||
const { sidebarsVisible, readerMode } = state;
|
||||
|
||||
// Determine if tool panel should be active based on state
|
||||
|
Loading…
Reference in New Issue
Block a user