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;
|
selectionMode: boolean;
|
||||||
isAnimating: boolean;
|
isAnimating: boolean;
|
||||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
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;
|
renderSplitMarker?: (item: T, index: number) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ interface PageThumbnailProps {
|
|||||||
selectionMode: boolean;
|
selectionMode: boolean;
|
||||||
movingPage: number | null;
|
movingPage: number | null;
|
||||||
isAnimating: boolean;
|
isAnimating: boolean;
|
||||||
pageRefs: React.Ref<Map<string, HTMLDivElement>>;
|
pageRefs: React.RefObject<Map<string, HTMLDivElement>>;
|
||||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
||||||
onTogglePage: (pageId: string) => void;
|
onTogglePage: (pageId: string) => void;
|
||||||
onAnimateReorder: () => void;
|
onAnimateReorder: () => void;
|
||||||
@ -65,7 +65,11 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [isMouseDown, setIsMouseDown] = useState(false);
|
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||||
const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null);
|
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 [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
||||||
const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
|
const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
|
||||||
const { openFilesModal } = useFilesModalContext();
|
const { openFilesModal } = useFilesModalContext();
|
||||||
@ -171,19 +175,15 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
type: 'page',
|
type: 'page',
|
||||||
pageNumber: page.pageNumber
|
pageNumber: page.pageNumber
|
||||||
}),
|
}),
|
||||||
onDrop: (_) => {}
|
onDrop: (_) => { /* empty */ }
|
||||||
});
|
});
|
||||||
|
|
||||||
dragElementRef.current.__dragCleanup = () => {
|
dragElementRef.current.__dragCleanup = () => {
|
||||||
dragCleanup();
|
dragCleanup();
|
||||||
dropCleanup();
|
dropCleanup();
|
||||||
};
|
};
|
||||||
} else {
|
if (dragElementRef.current?.__dragCleanup) {
|
||||||
if (pageRefs && 'current' in pageRefs && pageRefs.current) {
|
dragElementRef.current.__dragCleanup();
|
||||||
pageRefs.current.delete(page.id);
|
|
||||||
}
|
|
||||||
if (dragElementRef.current && (dragElementRef.current as any).__dragCleanup) {
|
|
||||||
dragElementRef.current.__dragCleanup?.();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [page.id, page.pageNumber, pageRefs, selectionMode, selectedPageIds, pdfDocument.pages, onReorderPages]);
|
}, [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 { OCRParameters } from 'src/hooks/tools/ocr/useOCRParameters';
|
||||||
import { TooltipTip } from 'src/types/tips';
|
import { TooltipTip } from 'src/types/tips';
|
||||||
import { RemovePasswordParameters } from 'src/hooks/tools/removePassword/useRemovePasswordParameters';
|
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 {
|
export interface FilesStepConfig {
|
||||||
selectedFiles: StirlingFile[];
|
selectedFiles: StirlingFile[];
|
||||||
@ -47,7 +65,29 @@ export interface ExecuteButtonConfig {
|
|||||||
|
|
||||||
export interface ReviewStepConfig {
|
export interface ReviewStepConfig {
|
||||||
isVisible: boolean;
|
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;
|
title: string;
|
||||||
onFileClick?: (file: File) => void;
|
onFileClick?: (file: File) => void;
|
||||||
onUndo: () => void;
|
onUndo: () => void;
|
||||||
@ -99,7 +139,11 @@ export function createToolFlow(config: ToolFlowConfig) {
|
|||||||
{/* Execute Button */}
|
{/* Execute Button */}
|
||||||
{config.executeButton && config.executeButton.isVisible !== false && (
|
{config.executeButton && config.executeButton.isVisible !== false && (
|
||||||
<OperationButton
|
<OperationButton
|
||||||
onClick={config.executeButton.onClick}
|
onClick={() => {
|
||||||
|
if (config.executeButton) {
|
||||||
|
config.executeButton.onClick().catch(console.error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
isLoading={config.review.operation.isLoading}
|
isLoading={config.review.operation.isLoading}
|
||||||
disabled={config.executeButton.disabled}
|
disabled={config.executeButton.disabled}
|
||||||
loadingText={config.executeButton.loadingText}
|
loadingText={config.executeButton.loadingText}
|
||||||
@ -109,9 +153,9 @@ export function createToolFlow(config: ToolFlowConfig) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Review Step */}
|
{/* Review Step */}
|
||||||
{steps.createReviewStep({
|
{steps.createReviewStep<SplitParameters | CompressParameters | ChangeMetadataParameters | OCRParameters | RemovePasswordParameters>({
|
||||||
isVisible: config.review.isVisible,
|
isVisible: config.review.isVisible,
|
||||||
operation: config.review.operation,
|
operation: config.review.operation as ToolOperationHook<SplitParameters | CompressParameters | ChangeMetadataParameters | OCRParameters | RemovePasswordParameters>,
|
||||||
title: config.review.title,
|
title: config.review.title,
|
||||||
onFileClick: config.review.onFileClick,
|
onFileClick: config.review.onFileClick,
|
||||||
onUndo: config.review.onUndo
|
onUndo: config.review.onUndo
|
||||||
|
@ -165,8 +165,8 @@ interface AddFileOptions {
|
|||||||
*/
|
*/
|
||||||
export async function addFiles(
|
export async function addFiles(
|
||||||
options: AddFileOptions,
|
options: AddFileOptions,
|
||||||
stateRef: React.Ref<FileContextState>,
|
stateRef: React.RefObject<FileContextState>,
|
||||||
filesRef: React.Ref<Map<FileId, File>>,
|
filesRef: React.RefObject<Map<FileId, File>>,
|
||||||
dispatch: React.Dispatch<FileContextAction>,
|
dispatch: React.Dispatch<FileContextAction>,
|
||||||
lifecycleManager: FileLifecycleManager,
|
lifecycleManager: FileLifecycleManager,
|
||||||
enablePersistence = false
|
enablePersistence = false
|
||||||
@ -204,7 +204,7 @@ export async function addFiles(
|
|||||||
let thumbnail: string | undefined;
|
let thumbnail: string | undefined;
|
||||||
if (processedFileMetadata) {
|
if (processedFileMetadata) {
|
||||||
// PDF file - use thumbnail from processedFile metadata
|
// 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`);
|
if (DEBUG) console.log(`📄 Generated PDF metadata for ${file.name}: ${processedFileMetadata.totalPages} pages, thumbnail: SUCCESS`);
|
||||||
} else if (!file.type.startsWith('application/pdf')) {
|
} else if (!file.type.startsWith('application/pdf')) {
|
||||||
// Non-PDF files: simple thumbnail generation, no processedFile metadata
|
// Non-PDF files: simple thumbnail generation, no processedFile metadata
|
||||||
@ -278,7 +278,7 @@ export async function consumeFiles(
|
|||||||
inputFileIds: FileId[],
|
inputFileIds: FileId[],
|
||||||
outputStirlingFiles: StirlingFile[],
|
outputStirlingFiles: StirlingFile[],
|
||||||
outputStirlingFileStubs: StirlingFileStub[],
|
outputStirlingFileStubs: StirlingFileStub[],
|
||||||
filesRef: React.Ref<Map<FileId, File>>,
|
filesRef: React.RefObject<Map<FileId, File>>,
|
||||||
dispatch: React.Dispatch<FileContextAction>
|
dispatch: React.Dispatch<FileContextAction>
|
||||||
): Promise<FileId[]> {
|
): Promise<FileId[]> {
|
||||||
if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputStirlingFiles.length} output files with pre-created stubs`);
|
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(
|
async function restoreFilesAndCleanup(
|
||||||
filesToRestore: { file: File; record: StirlingFileStub }[],
|
filesToRestore: { file: File; record: StirlingFileStub }[],
|
||||||
fileIdsToRemove: FileId[],
|
fileIdsToRemove: FileId[],
|
||||||
filesRef: React.Ref<Map<FileId, File>>,
|
filesRef: React.RefObject<Map<FileId, File>>,
|
||||||
indexedDB?: { deleteFile: (fileId: FileId) => Promise<void> } | null
|
indexedDB?: { deleteFile: (fileId: FileId) => Promise<void> } | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Remove files from filesRef
|
// Remove files from filesRef
|
||||||
@ -406,7 +406,7 @@ export async function undoConsumeFiles(
|
|||||||
inputFiles: File[],
|
inputFiles: File[],
|
||||||
inputStirlingFileStubs: StirlingFileStub[],
|
inputStirlingFileStubs: StirlingFileStub[],
|
||||||
outputFileIds: FileId[],
|
outputFileIds: FileId[],
|
||||||
filesRef: React.Ref<Map<FileId, File>>,
|
filesRef: React.RefObject<Map<FileId, File>>,
|
||||||
dispatch: React.Dispatch<FileContextAction>,
|
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<any>; deleteFile: (fileId: FileId) => Promise<void> } | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -468,8 +468,8 @@ export async function undoConsumeFiles(
|
|||||||
export async function addStirlingFileStubs(
|
export async function addStirlingFileStubs(
|
||||||
stirlingFileStubs: StirlingFileStub[],
|
stirlingFileStubs: StirlingFileStub[],
|
||||||
options: { insertAfterPageId?: string; selectFiles?: boolean } = {},
|
options: { insertAfterPageId?: string; selectFiles?: boolean } = {},
|
||||||
stateRef: React.Ref<FileContextState>,
|
stateRef: React.RefObject<FileContextState>,
|
||||||
filesRef: React.Ref<Map<FileId, File>>,
|
filesRef: React.RefObject<Map<FileId, File>>,
|
||||||
dispatch: React.Dispatch<FileContextAction>,
|
dispatch: React.Dispatch<FileContextAction>,
|
||||||
_lifecycleManager: FileLifecycleManager
|
_lifecycleManager: FileLifecycleManager
|
||||||
): Promise<StirlingFile[]> {
|
): Promise<StirlingFile[]> {
|
||||||
|
@ -15,8 +15,8 @@ import {
|
|||||||
* Create stable selectors using stateRef and filesRef
|
* Create stable selectors using stateRef and filesRef
|
||||||
*/
|
*/
|
||||||
export function createFileSelectors(
|
export function createFileSelectors(
|
||||||
stateRef: React.Ref<FileContextState>,
|
stateRef: React.RefObject<FileContextState>,
|
||||||
filesRef: React.Ref<Map<FileId, File>>
|
filesRef: React.RefObject<Map<FileId, File>>
|
||||||
): FileContextSelectors {
|
): FileContextSelectors {
|
||||||
return {
|
return {
|
||||||
getFile: (id: FileId) => {
|
getFile: (id: FileId) => {
|
||||||
@ -125,8 +125,8 @@ export function buildQuickKeySetFromMetadata(metadata: { name: string; size: num
|
|||||||
* Get primary file (first in list) - commonly used pattern
|
* Get primary file (first in list) - commonly used pattern
|
||||||
*/
|
*/
|
||||||
export function getPrimaryFile(
|
export function getPrimaryFile(
|
||||||
stateRef: React.Ref<FileContextState>,
|
stateRef: React.RefObject<FileContextState>,
|
||||||
filesRef: React.Ref<Map<FileId, File>>
|
filesRef: React.RefObject<Map<FileId, File>>
|
||||||
): { file?: File; record?: StirlingFileStub } {
|
): { file?: File; record?: StirlingFileStub } {
|
||||||
const primaryFileId = stateRef.current.files.ids[0];
|
const primaryFileId = stateRef.current.files.ids[0];
|
||||||
if (!primaryFileId) return {};
|
if (!primaryFileId) return {};
|
||||||
|
@ -16,7 +16,7 @@ export class FileLifecycleManager {
|
|||||||
private fileGenerations = new Map<string, number>(); // Generation tokens to prevent stale cleanup
|
private fileGenerations = new Map<string, number>(); // Generation tokens to prevent stale cleanup
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private filesRef: React.Ref<Map<FileId, File>>,
|
private filesRef: React.RefObject<Map<FileId, File> | null>,
|
||||||
private dispatch: React.Dispatch<FileContextAction>
|
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)
|
* 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)
|
// Use comprehensive cleanup (same as removeFiles)
|
||||||
this.cleanupAllResourcesForFile(fileId, stateRef);
|
this.cleanupAllResourcesForFile(fileId, stateRef);
|
||||||
|
|
||||||
@ -62,13 +62,13 @@ export class FileLifecycleManager {
|
|||||||
this.fileGenerations.clear();
|
this.fileGenerations.clear();
|
||||||
|
|
||||||
// Clear files ref
|
// Clear files ref
|
||||||
this.filesRef.current.clear();
|
this.filesRef.current?.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule delayed cleanup for a file with generation token to prevent stale cleanup
|
* 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
|
// Cancel existing timer
|
||||||
const existingTimer = this.cleanupTimers.get(fileId);
|
const existingTimer = this.cleanupTimers.get(fileId);
|
||||||
if (existingTimer) {
|
if (existingTimer) {
|
||||||
@ -101,7 +101,7 @@ export class FileLifecycleManager {
|
|||||||
/**
|
/**
|
||||||
* Remove a file immediately with complete resource cleanup
|
* 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 => {
|
fileIds.forEach(fileId => {
|
||||||
// Clean up all resources for this file
|
// Clean up all resources for this file
|
||||||
this.cleanupAllResourcesForFile(fileId, stateRef);
|
this.cleanupAllResourcesForFile(fileId, stateRef);
|
||||||
@ -114,9 +114,9 @@ export class FileLifecycleManager {
|
|||||||
/**
|
/**
|
||||||
* Complete resource cleanup for a single file
|
* 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
|
// Remove from files ref
|
||||||
this.filesRef.current.delete(fileId);
|
this.filesRef.current?.delete(fileId);
|
||||||
|
|
||||||
// Cancel cleanup timer and generation
|
// Cancel cleanup timer and generation
|
||||||
const timer = this.cleanupTimers.get(fileId);
|
const timer = this.cleanupTimers.get(fileId);
|
||||||
@ -166,15 +166,15 @@ export class FileLifecycleManager {
|
|||||||
/**
|
/**
|
||||||
* Update file record with race condition guards
|
* 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)
|
// 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}`);
|
if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional state guard for rare race conditions
|
// 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}`);
|
if (DEBUG) console.warn(`🗂️ Attempted to update removed file (state): ${fileId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ export function useSuggestedAutomations(): SuggestedAutomation[] {
|
|||||||
{
|
{
|
||||||
operation: "ocr",
|
operation: "ocr",
|
||||||
parameters: {
|
parameters: {
|
||||||
languages: ['eng'],
|
languages: ['eng'] as (string | null)[],
|
||||||
ocrType: 'skip-text',
|
ocrType: 'skip-text',
|
||||||
ocrRenderType: 'hocr',
|
ocrRenderType: 'hocr',
|
||||||
additionalOptions: ['clean', 'cleanFinal'],
|
additionalOptions: ['clean', 'cleanFinal'],
|
||||||
@ -175,7 +175,7 @@ export function useSuggestedAutomations(): SuggestedAutomation[] {
|
|||||||
{
|
{
|
||||||
operation: "ocr",
|
operation: "ocr",
|
||||||
parameters: {
|
parameters: {
|
||||||
languages: ['eng'],
|
languages: ['eng'] as (string | null)[],
|
||||||
ocrType: 'skip-text',
|
ocrType: 'skip-text',
|
||||||
ocrRenderType: 'hocr',
|
ocrRenderType: 'hocr',
|
||||||
additionalOptions: [],
|
additionalOptions: [],
|
||||||
|
@ -44,11 +44,15 @@ export const useToolApiCalls = <TParams = void>() => {
|
|||||||
|
|
||||||
// Forward to shared response processor (uses tool-specific responseHandler if provided)
|
// Forward to shared response processor (uses tool-specific responseHandler if provided)
|
||||||
const responseFiles = await processResponse(
|
const responseFiles = await processResponse(
|
||||||
response.data,
|
response.data as Blob,
|
||||||
[file],
|
[file],
|
||||||
config.filePrefix,
|
config.filePrefix,
|
||||||
config.responseHandler,
|
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);
|
processedFiles.push(...responseFiles);
|
||||||
|
|
||||||
|
@ -36,10 +36,10 @@ export const useDocumentMeta = (meta: MetaOptions) => {
|
|||||||
let metaElement = document.querySelector(`meta[name="${name}"]`)!;
|
let metaElement = document.querySelector(`meta[name="${name}"]`)!;
|
||||||
if (!metaElement) {
|
if (!metaElement) {
|
||||||
metaElement = document.createElement('meta');
|
metaElement = document.createElement('meta');
|
||||||
metaElement.name = name;
|
(metaElement as HTMLMetaElement).name = name;
|
||||||
document.head.appendChild(metaElement);
|
document.head.appendChild(metaElement);
|
||||||
}
|
}
|
||||||
metaElement.content = content;
|
(metaElement as HTMLMetaElement).content = content;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateOrCreateProperty = (property: string, content: string) => {
|
const updateOrCreateProperty = (property: string, content: string) => {
|
||||||
@ -49,7 +49,7 @@ export const useDocumentMeta = (meta: MetaOptions) => {
|
|||||||
metaElement.setAttribute('property', property);
|
metaElement.setAttribute('property', property);
|
||||||
document.head.appendChild(metaElement);
|
document.head.appendChild(metaElement);
|
||||||
}
|
}
|
||||||
metaElement.content = content;
|
(metaElement as HTMLMetaElement).content = content;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update meta tags
|
// Update meta tags
|
||||||
@ -90,7 +90,7 @@ export const useDocumentMeta = (meta: MetaOptions) => {
|
|||||||
const element = document.querySelector(`meta[property="${property}"]`)!;
|
const element = document.querySelector(`meta[property="${property}"]`)!;
|
||||||
if (element) {
|
if (element) {
|
||||||
if (originalValue !== null) {
|
if (originalValue !== null) {
|
||||||
element.content = originalValue;
|
(element as HTMLMetaElement).content = originalValue;
|
||||||
} else {
|
} else {
|
||||||
element.remove();
|
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
|
// State to track overflow status
|
||||||
const [isOverflow, setIsOverflow] = React.useState<boolean | undefined>(undefined);
|
const [isOverflow, setIsOverflow] = React.useState<boolean | undefined>(undefined);
|
||||||
|
|
||||||
|
@ -60,8 +60,8 @@ export function useTooltipPosition({
|
|||||||
sidebarTooltip: boolean;
|
sidebarTooltip: boolean;
|
||||||
position: Position;
|
position: Position;
|
||||||
gap: number;
|
gap: number;
|
||||||
triggerRef: React.Ref<HTMLElement | null>;
|
triggerRef: React.RefObject<HTMLElement | null>;
|
||||||
tooltipRef: React.Ref<HTMLDivElement | null>;
|
tooltipRef: React.RefObject<HTMLDivElement | null>;
|
||||||
sidebarRefs?: SidebarRefs;
|
sidebarRefs?: SidebarRefs;
|
||||||
sidebarState?: SidebarState;
|
sidebarState?: SidebarState;
|
||||||
}): PositionState {
|
}): PositionState {
|
||||||
|
@ -173,8 +173,10 @@ class AutomationStorage {
|
|||||||
const lowerQuery = query.toLowerCase();
|
const lowerQuery = query.toLowerCase();
|
||||||
return automations.filter(automation =>
|
return automations.filter(automation =>
|
||||||
automation.name.toLowerCase().includes(lowerQuery) ||
|
automation.name.toLowerCase().includes(lowerQuery) ||
|
||||||
(automation.description?.toLowerCase().includes(lowerQuery)) ??
|
((automation.description?.toLowerCase().includes(lowerQuery)) ?? false),
|
||||||
automation.operations.some(op => op.operation.toLowerCase().includes(lowerQuery))
|
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 { useNavigationActions } from "../contexts/NavigationContext";
|
||||||
import { useToolWorkflow } from "../contexts/ToolWorkflowContext";
|
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 { createFilesToolStep } from "../components/tools/shared/FilesToolStep";
|
||||||
import AutomationSelection from "../components/tools/automate/AutomationSelection";
|
import AutomationSelection from "../components/tools/automate/AutomationSelection";
|
||||||
import AutomationCreation from "../components/tools/automate/AutomationCreation";
|
import AutomationCreation from "../components/tools/automate/AutomationCreation";
|
||||||
@ -14,8 +14,9 @@ import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperati
|
|||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps } from "../types/tool";
|
||||||
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
|
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
|
||||||
import { useSavedAutomations } from "../hooks/tools/automate/useSavedAutomations";
|
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 { AUTOMATION_STEPS } from "../constants/automation";
|
||||||
|
import { StirlingFile } from "src/types/fileContext";
|
||||||
|
|
||||||
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -146,7 +147,13 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
<AutomationRun
|
<AutomationRun
|
||||||
automation={stepData.automation}
|
automation={stepData.automation}
|
||||||
onComplete={handleComplete}
|
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) => ({
|
const createStep = (title: string, props: Record<string, unknown>, content?: React.ReactNode): React.ReactElement => {
|
||||||
title,
|
return (
|
||||||
...props,
|
<div {...props}>
|
||||||
content
|
<h3>{title}</h3>
|
||||||
});
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Always create files step to avoid conditional hook calls
|
// Always create files step to avoid conditional hook calls
|
||||||
const filesStep = createFilesToolStep(createStep, {
|
const filesStep = createFilesToolStep(createStep, {
|
||||||
@ -167,8 +177,10 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
isCollapsed: hasResults,
|
isCollapsed: hasResults,
|
||||||
});
|
});
|
||||||
|
|
||||||
const automationSteps = [
|
const automationSteps: MiddleStepConfig[] = [
|
||||||
createStep(t('automate.selection.title', 'Automation Selection'), {
|
{
|
||||||
|
title: t('automate.selection.title', 'Automation Selection'),
|
||||||
|
content: currentStep === AUTOMATION_STEPS.SELECTION ? renderCurrentStep() : null,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isCollapsed: currentStep !== AUTOMATION_STEPS.SELECTION,
|
isCollapsed: currentStep !== AUTOMATION_STEPS.SELECTION,
|
||||||
onCollapsedClick: () => {
|
onCollapsedClick: () => {
|
||||||
@ -177,26 +189,27 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
setCurrentStep(AUTOMATION_STEPS.SELECTION);
|
setCurrentStep(AUTOMATION_STEPS.SELECTION);
|
||||||
setStepData({ step: AUTOMATION_STEPS.SELECTION });
|
setStepData({ step: AUTOMATION_STEPS.SELECTION });
|
||||||
}
|
}
|
||||||
}, currentStep === AUTOMATION_STEPS.SELECTION ? renderCurrentStep() : null),
|
},
|
||||||
|
{
|
||||||
createStep(stepData.mode === AutomationMode.EDIT
|
title: stepData.mode === AutomationMode.EDIT
|
||||||
? t('automate.creation.editTitle', 'Edit Automation')
|
? t('automate.creation.editTitle', 'Edit Automation')
|
||||||
: t('automate.creation.createTitle', 'Create Automation'), {
|
: t('automate.creation.createTitle', 'Create Automation'),
|
||||||
|
content: currentStep === AUTOMATION_STEPS.CREATION ? renderCurrentStep() : null,
|
||||||
isVisible: currentStep === AUTOMATION_STEPS.CREATION,
|
isVisible: currentStep === AUTOMATION_STEPS.CREATION,
|
||||||
isCollapsed: false
|
isCollapsed: false
|
||||||
}, currentStep === AUTOMATION_STEPS.CREATION ? renderCurrentStep() : null),
|
},
|
||||||
|
|
||||||
// Files step - only visible during run mode
|
|
||||||
{
|
{
|
||||||
...filesStep,
|
...filesStep,
|
||||||
|
title: t('automate.files.title', 'Files'),
|
||||||
|
content: null, // Files step content is managed separately
|
||||||
isVisible: currentStep === AUTOMATION_STEPS.RUN
|
isVisible: currentStep === AUTOMATION_STEPS.RUN
|
||||||
},
|
},
|
||||||
|
{
|
||||||
// Run step
|
title: t('automate.run.title', 'Run Automation'),
|
||||||
createStep(t('automate.run.title', 'Run Automation'), {
|
content: currentStep === AUTOMATION_STEPS.RUN ? renderCurrentStep() : null,
|
||||||
isVisible: currentStep === AUTOMATION_STEPS.RUN,
|
isVisible: currentStep === AUTOMATION_STEPS.RUN,
|
||||||
isCollapsed: hasResults,
|
isCollapsed: hasResults
|
||||||
}, currentStep === AUTOMATION_STEPS.RUN ? renderCurrentStep() : null)
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return createToolFlow({
|
return createToolFlow({
|
||||||
@ -214,7 +227,11 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
onPreviewFile?.(file);
|
onPreviewFile?.(file);
|
||||||
actions.setWorkbench('viewer');
|
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,
|
operation: base.operation,
|
||||||
title: t("removePages.results.title", "Pages Removed"),
|
title: t("removePages.results.title", "Pages Removed"),
|
||||||
onFileClick: base.handleThumbnailClick,
|
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,
|
operation: base.operation,
|
||||||
title: t("rotate.title", "Rotation Results"),
|
title: t("rotate.title", "Rotation Results"),
|
||||||
onFileClick: base.handleThumbnailClick,
|
onFileClick: base.handleThumbnailClick,
|
||||||
onUndo: base.handleUndo,
|
onUndo: () => {
|
||||||
|
base.handleUndo().catch((error) => {
|
||||||
|
console.error("Undo operation failed:", error);
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
export interface AutomationOperation {
|
export interface AutomationOperation {
|
||||||
operation: string;
|
operation: string;
|
||||||
parameters: Record<string, string | number | boolean | null>;
|
parameters: Record<string, JsonValue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutomationConfig {
|
export interface AutomationConfig {
|
||||||
@ -22,7 +22,7 @@ export interface AutomationTool {
|
|||||||
operation: string;
|
operation: string;
|
||||||
name: string;
|
name: string;
|
||||||
configured: boolean;
|
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];
|
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;
|
onStepError?: (stepIndex: number, error: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutomateParameters extends AutomationExecutionCallbacks {
|
|
||||||
automationConfig?: AutomationConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AutomationMode {
|
export enum AutomationMode {
|
||||||
CREATE = 'create',
|
CREATE = 'create',
|
||||||
EDIT = 'edit',
|
EDIT = 'edit',
|
||||||
@ -71,3 +67,49 @@ export interface SuggestedAutomation {
|
|||||||
export interface AutomateParameters extends AutomationExecutionCallbacks {
|
export interface AutomateParameters extends AutomationExecutionCallbacks {
|
||||||
automationConfig?: AutomationConfig;
|
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 {
|
export interface SidebarRefs {
|
||||||
quickAccessRef: React.Ref<HTMLDivElement | null>;
|
quickAccessRef: React.RefObject<HTMLDivElement | null>;
|
||||||
toolPanelRef: React.Ref<HTMLDivElement | null>;
|
toolPanelRef: React.RefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SidebarInfo {
|
export interface SidebarInfo {
|
||||||
|
@ -62,11 +62,15 @@ export const executeToolOperationWithPrefix = async (
|
|||||||
timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
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)
|
// Multi-file responses are typically ZIP files, but may be single files (e.g. split with merge=true)
|
||||||
let result;
|
let result;
|
||||||
if (response.data.type === 'application/pdf' ||
|
if ((response.data as Blob).type === 'application/pdf' ||
|
||||||
(response.headers && response.headers['content-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
|
// Single PDF response (e.g. split with merge option) - use processResponse to respect preserveBackendFilename
|
||||||
const processedFiles = await processResponse(
|
const processedFiles = await processResponse(
|
||||||
@ -85,7 +89,11 @@ export const executeToolOperationWithPrefix = async (
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// ZIP response
|
// 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) {
|
if (result.errors.length > 0) {
|
||||||
@ -123,15 +131,21 @@ export const executeToolOperationWithPrefix = async (
|
|||||||
timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
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
|
// Create result file using processResponse to respect preserveBackendFilename setting
|
||||||
const processedFiles = await processResponse(
|
const processedFiles = await processResponse(
|
||||||
response.data,
|
response.data as Blob,
|
||||||
[file],
|
[file],
|
||||||
filePrefix,
|
filePrefix,
|
||||||
undefined,
|
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);
|
resultFiles.push(...processedFiles);
|
||||||
console.log(`✅ Created result file(s): ${processedFiles.map(f => f.name).join(', ')}`);
|
console.log(`✅ Created result file(s): ${processedFiles.map(f => f.name).join(', ')}`);
|
||||||
@ -141,17 +155,26 @@ export const executeToolOperationWithPrefix = async (
|
|||||||
return resultFiles;
|
return resultFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error(`Tool operation ${operationName} failed:`, error);
|
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
|
* Execute an entire automation sequence
|
||||||
*/
|
*/
|
||||||
|
interface Automation {
|
||||||
|
name?: string;
|
||||||
|
operations: { operation: string; parameters?: Record<string, unknown> }[];
|
||||||
|
}
|
||||||
|
|
||||||
export const executeAutomationSequence = async (
|
export const executeAutomationSequence = async (
|
||||||
automation: any,
|
automation: Automation,
|
||||||
initialFiles: File[],
|
initialFiles: File[],
|
||||||
toolRegistry: ToolRegistry,
|
toolRegistry: ToolRegistry,
|
||||||
onStepStart?: (stepIndex: number, operationName: string) => void,
|
onStepStart?: (stepIndex: number, operationName: string) => void,
|
||||||
@ -191,9 +214,9 @@ export const executeAutomationSequence = async (
|
|||||||
currentFiles = resultFiles;
|
currentFiles = resultFiles;
|
||||||
onStepComplete?.(i, resultFiles);
|
onStepComplete?.(i, resultFiles);
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error(`❌ Step ${i + 1} failed:`, error);
|
console.error(`❌ Step ${i + 1} failed:`, error);
|
||||||
onStepError?.(i, error.message);
|
onStepError?.(i, error instanceof Error ? error.message : 'Unknown error');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ export class FileHasher {
|
|||||||
offset += chunk.byteLength;
|
offset += chunk.byteLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
return combined.buffer;
|
return Promise.resolve(combined.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async hashArrayBuffer(buffer: ArrayBuffer): Promise<string> {
|
private static async hashArrayBuffer(buffer: ArrayBuffer): Promise<string> {
|
||||||
|
@ -135,7 +135,7 @@ describe('fileResponseUtils', () => {
|
|||||||
|
|
||||||
test('should handle null/undefined headers gracefully', () => {
|
test('should handle null/undefined headers gracefully', () => {
|
||||||
const responseData = new Uint8Array([1, 2, 3, 4]);
|
const responseData = new Uint8Array([1, 2, 3, 4]);
|
||||||
const headers = null;
|
const headers = {} as Record<string, string | undefined>;
|
||||||
const fallbackFilename = 'test.bin';
|
const fallbackFilename = 'test.bin';
|
||||||
|
|
||||||
const file = createFileFromApiResponse(responseData, headers, fallbackFilename);
|
const file = createFileFromApiResponse(responseData, headers, fallbackFilename);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { RefObject } from 'react';
|
||||||
import { SidebarRefs, SidebarState, SidebarInfo } from '../types/sidebar';
|
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
|
* @returns Object containing the sidebar rect and whether the tool panel is active
|
||||||
*/
|
*/
|
||||||
export function getSidebarInfo(refs: SidebarRefs, state: SidebarState): SidebarInfo {
|
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;
|
const { sidebarsVisible, readerMode } = state;
|
||||||
|
|
||||||
// Determine if tool panel should be active based on state
|
// Determine if tool panel should be active based on state
|
||||||
|
Loading…
Reference in New Issue
Block a user