mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-08-02 13:48:15 +02:00
Implement file selection context and integrate with tool management; refactor FileEditor and ToolRenderer for improved file handling
This commit is contained in:
parent
0549c5b191
commit
405cfd8161
@ -7,6 +7,7 @@ import { Dropzone } from '@mantine/dropzone';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
||||||
import { useFileContext } from '../../contexts/FileContext';
|
import { useFileContext } from '../../contexts/FileContext';
|
||||||
|
import { useFileSelection } from '../../contexts/FileSelectionContext';
|
||||||
import { FileOperation } from '../../types/fileContext';
|
import { FileOperation } from '../../types/fileContext';
|
||||||
import { fileStorage } from '../../services/fileStorage';
|
import { fileStorage } from '../../services/fileStorage';
|
||||||
import { generateThumbnailForFile } from '../../utils/thumbnailUtils';
|
import { generateThumbnailForFile } from '../../utils/thumbnailUtils';
|
||||||
@ -31,20 +32,16 @@ interface FileEditorProps {
|
|||||||
onOpenPageEditor?: (file: File) => void;
|
onOpenPageEditor?: (file: File) => void;
|
||||||
onMergeFiles?: (files: File[]) => void;
|
onMergeFiles?: (files: File[]) => void;
|
||||||
toolMode?: boolean;
|
toolMode?: boolean;
|
||||||
multiSelect?: boolean;
|
|
||||||
showUpload?: boolean;
|
showUpload?: boolean;
|
||||||
showBulkActions?: boolean;
|
showBulkActions?: boolean;
|
||||||
onFileSelect?: (files: File[]) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileEditor = ({
|
const FileEditor = ({
|
||||||
onOpenPageEditor,
|
onOpenPageEditor,
|
||||||
onMergeFiles,
|
onMergeFiles,
|
||||||
toolMode = false,
|
toolMode = false,
|
||||||
multiSelect = true,
|
|
||||||
showUpload = true,
|
showUpload = true,
|
||||||
showBulkActions = true,
|
showBulkActions = true
|
||||||
onFileSelect
|
|
||||||
}: FileEditorProps) => {
|
}: FileEditorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -63,6 +60,14 @@ const FileEditor = ({
|
|||||||
markOperationApplied
|
markOperationApplied
|
||||||
} = fileContext;
|
} = fileContext;
|
||||||
|
|
||||||
|
// Get file selection context
|
||||||
|
const {
|
||||||
|
selectedFiles: toolSelectedFiles,
|
||||||
|
setSelectedFiles: setToolSelectedFiles,
|
||||||
|
maxFiles,
|
||||||
|
isToolMode
|
||||||
|
} = useFileSelection();
|
||||||
|
|
||||||
const [files, setFiles] = useState<FileItem[]>([]);
|
const [files, setFiles] = useState<FileItem[]>([]);
|
||||||
const [status, setStatus] = useState<string | null>(null);
|
const [status, setStatus] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@ -99,14 +104,14 @@ const FileEditor = ({
|
|||||||
const lastActiveFilesRef = useRef<string[]>([]);
|
const lastActiveFilesRef = useRef<string[]>([]);
|
||||||
const lastProcessedFilesRef = useRef<number>(0);
|
const lastProcessedFilesRef = useRef<number>(0);
|
||||||
|
|
||||||
// Map context selected file names to local file IDs
|
// Get selected file IDs from context (defensive programming)
|
||||||
// Defensive programming: ensure selectedFileIds is always an array
|
const contextSelectedIds = Array.isArray(selectedFileIds) ? selectedFileIds : [];
|
||||||
const safeSelectedFileIds = Array.isArray(selectedFileIds) ? selectedFileIds : [];
|
|
||||||
|
|
||||||
const localSelectedFiles = files
|
// Map context selections to local file IDs for UI display
|
||||||
|
const localSelectedIds = files
|
||||||
.filter(file => {
|
.filter(file => {
|
||||||
const fileId = (file.file as any).id || file.name;
|
const fileId = (file.file as any).id || file.name;
|
||||||
return safeSelectedFileIds.includes(fileId);
|
return contextSelectedIds.includes(fileId);
|
||||||
})
|
})
|
||||||
.map(file => file.id);
|
.map(file => file.id);
|
||||||
|
|
||||||
@ -396,44 +401,41 @@ const FileEditor = ({
|
|||||||
if (!targetFile) return;
|
if (!targetFile) return;
|
||||||
|
|
||||||
const contextFileId = (targetFile.file as any).id || targetFile.name;
|
const contextFileId = (targetFile.file as any).id || targetFile.name;
|
||||||
|
const isSelected = contextSelectedIds.includes(contextFileId);
|
||||||
|
|
||||||
if (!multiSelect) {
|
let newSelection: string[];
|
||||||
// Single select mode for tools - toggle on/off
|
|
||||||
const isCurrentlySelected = safeSelectedFileIds.includes(contextFileId);
|
if (isSelected) {
|
||||||
if (isCurrentlySelected) {
|
// Remove file from selection
|
||||||
// Deselect the file
|
newSelection = contextSelectedIds.filter(id => id !== contextFileId);
|
||||||
setContextSelectedFiles([]);
|
|
||||||
if (onFileSelect) {
|
|
||||||
onFileSelect([]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Select the file
|
|
||||||
setContextSelectedFiles([contextFileId]);
|
|
||||||
if (onFileSelect) {
|
|
||||||
onFileSelect([targetFile.file]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Multi select mode (default)
|
// Add file to selection
|
||||||
setContextSelectedFiles(prev => {
|
if (maxFiles === 1) {
|
||||||
const safePrev = Array.isArray(prev) ? prev : [];
|
newSelection = [contextFileId];
|
||||||
return safePrev.includes(contextFileId)
|
} else {
|
||||||
? safePrev.filter(id => id !== contextFileId)
|
// Check if we've hit the selection limit
|
||||||
: [...safePrev, contextFileId];
|
if (maxFiles > 1 && contextSelectedIds.length >= maxFiles) {
|
||||||
});
|
setStatus(`Maximum ${maxFiles} files can be selected`);
|
||||||
|
return;
|
||||||
// Notify parent with selected files
|
}
|
||||||
if (onFileSelect) {
|
newSelection = [...contextSelectedIds, contextFileId];
|
||||||
const selectedFiles = files
|
|
||||||
.filter(f => {
|
|
||||||
const fId = (f.file as any).id || f.name;
|
|
||||||
return safeSelectedFileIds.includes(fId) || fId === contextFileId;
|
|
||||||
})
|
|
||||||
.map(f => f.file);
|
|
||||||
onFileSelect(selectedFiles);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [files, setContextSelectedFiles, multiSelect, onFileSelect, safeSelectedFileIds]);
|
|
||||||
|
// Update context
|
||||||
|
setContextSelectedFiles(newSelection);
|
||||||
|
|
||||||
|
// Update tool selection context if in tool mode
|
||||||
|
if (isToolMode || toolMode) {
|
||||||
|
const selectedFiles = files
|
||||||
|
.filter(f => {
|
||||||
|
const fId = (f.file as any).id || f.name;
|
||||||
|
return newSelection.includes(fId);
|
||||||
|
})
|
||||||
|
.map(f => f.file);
|
||||||
|
setToolSelectedFiles(selectedFiles);
|
||||||
|
}
|
||||||
|
}, [files, setContextSelectedFiles, maxFiles, contextSelectedIds, setStatus, isToolMode, toolMode, setToolSelectedFiles]);
|
||||||
|
|
||||||
const toggleSelectionMode = useCallback(() => {
|
const toggleSelectionMode = useCallback(() => {
|
||||||
setSelectionMode(prev => {
|
setSelectionMode(prev => {
|
||||||
@ -450,15 +452,15 @@ const FileEditor = ({
|
|||||||
const handleDragStart = useCallback((fileId: string) => {
|
const handleDragStart = useCallback((fileId: string) => {
|
||||||
setDraggedFile(fileId);
|
setDraggedFile(fileId);
|
||||||
|
|
||||||
if (selectionMode && localSelectedFiles.includes(fileId) && localSelectedFiles.length > 1) {
|
if (selectionMode && localSelectedIds.includes(fileId) && localSelectedIds.length > 1) {
|
||||||
setMultiFileDrag({
|
setMultiFileDrag({
|
||||||
fileIds: localSelectedFiles,
|
fileIds: localSelectedIds,
|
||||||
count: localSelectedFiles.length
|
count: localSelectedIds.length
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setMultiFileDrag(null);
|
setMultiFileDrag(null);
|
||||||
}
|
}
|
||||||
}, [selectionMode, localSelectedFiles]);
|
}, [selectionMode, localSelectedIds]);
|
||||||
|
|
||||||
const handleDragEnd = useCallback(() => {
|
const handleDragEnd = useCallback(() => {
|
||||||
setDraggedFile(null);
|
setDraggedFile(null);
|
||||||
@ -519,8 +521,8 @@ const FileEditor = ({
|
|||||||
if (targetIndex === -1) return;
|
if (targetIndex === -1) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filesToMove = selectionMode && localSelectedFiles.includes(draggedFile)
|
const filesToMove = selectionMode && localSelectedIds.includes(draggedFile)
|
||||||
? localSelectedFiles
|
? localSelectedIds
|
||||||
: [draggedFile];
|
: [draggedFile];
|
||||||
|
|
||||||
// Update the local files state and sync with activeFiles
|
// Update the local files state and sync with activeFiles
|
||||||
@ -545,7 +547,7 @@ const FileEditor = ({
|
|||||||
const moveCount = multiFileDrag ? multiFileDrag.count : 1;
|
const moveCount = multiFileDrag ? multiFileDrag.count : 1;
|
||||||
setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`);
|
setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`);
|
||||||
|
|
||||||
}, [draggedFile, files, selectionMode, localSelectedFiles, multiFileDrag]);
|
}, [draggedFile, files, selectionMode, localSelectedIds, multiFileDrag]);
|
||||||
|
|
||||||
const handleEndZoneDragEnter = useCallback(() => {
|
const handleEndZoneDragEnter = useCallback(() => {
|
||||||
if (draggedFile) {
|
if (draggedFile) {
|
||||||
@ -764,7 +766,7 @@ const FileEditor = ({
|
|||||||
) : (
|
) : (
|
||||||
<DragDropGrid
|
<DragDropGrid
|
||||||
items={files}
|
items={files}
|
||||||
selectedItems={localSelectedFiles}
|
selectedItems={localSelectedIds}
|
||||||
selectionMode={selectionMode}
|
selectionMode={selectionMode}
|
||||||
isAnimating={isAnimating}
|
isAnimating={isAnimating}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
@ -783,7 +785,7 @@ const FileEditor = ({
|
|||||||
file={file}
|
file={file}
|
||||||
index={index}
|
index={index}
|
||||||
totalFiles={files.length}
|
totalFiles={files.length}
|
||||||
selectedFiles={localSelectedFiles}
|
selectedFiles={localSelectedIds}
|
||||||
selectionMode={selectionMode}
|
selectionMode={selectionMode}
|
||||||
draggedFile={draggedFile}
|
draggedFile={draggedFile}
|
||||||
dropTarget={dropTarget}
|
dropTarget={dropTarget}
|
||||||
|
@ -1,15 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Box, Text, Stack, Button, TextInput, Group } from "@mantine/core";
|
import { Box, Text, Stack, Button, TextInput, Group } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ToolRegistry } from "../../types/tool";
|
||||||
type Tool = {
|
|
||||||
icon: React.ReactNode;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ToolRegistry = {
|
|
||||||
[id: string]: Tool;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ToolPickerProps {
|
interface ToolPickerProps {
|
||||||
selectedToolKey: string | null;
|
selectedToolKey: string | null;
|
||||||
|
@ -1,23 +1,29 @@
|
|||||||
import { FileWithUrl } from "../../types/file";
|
import React, { Suspense } from "react";
|
||||||
|
import { Loader, Center, Stack, Text } from "@mantine/core";
|
||||||
import { useToolManagement } from "../../hooks/useToolManagement";
|
import { useToolManagement } from "../../hooks/useToolManagement";
|
||||||
|
import { BaseToolProps } from "../../types/tool";
|
||||||
|
|
||||||
interface ToolRendererProps {
|
interface ToolRendererProps extends BaseToolProps {
|
||||||
selectedToolKey: string;
|
selectedToolKey: string;
|
||||||
pdfFile: any;
|
|
||||||
files: FileWithUrl[];
|
|
||||||
toolParams: any;
|
|
||||||
updateParams: (params: any) => void;
|
|
||||||
toolSelectedFiles?: File[];
|
|
||||||
onPreviewFile?: (file: File | null) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Loading fallback component for lazy-loaded tools
|
||||||
|
const ToolLoadingFallback = ({ toolName }: { toolName?: string }) => (
|
||||||
|
<Center h="100%" w="100%">
|
||||||
|
<Stack align="center" gap="md">
|
||||||
|
<Loader size="lg" />
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
{toolName ? `Loading ${toolName}...` : "Loading tool..."}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
|
||||||
const ToolRenderer = ({
|
const ToolRenderer = ({
|
||||||
selectedToolKey,
|
selectedToolKey,
|
||||||
files,
|
|
||||||
toolParams,
|
|
||||||
updateParams,
|
|
||||||
toolSelectedFiles = [],
|
|
||||||
onPreviewFile,
|
onPreviewFile,
|
||||||
|
onComplete,
|
||||||
|
onError,
|
||||||
}: ToolRendererProps) => {
|
}: ToolRendererProps) => {
|
||||||
// Get the tool from registry
|
// Get the tool from registry
|
||||||
const { toolRegistry } = useToolManagement();
|
const { toolRegistry } = useToolManagement();
|
||||||
@ -29,41 +35,16 @@ files,
|
|||||||
|
|
||||||
const ToolComponent = selectedTool.component;
|
const ToolComponent = selectedTool.component;
|
||||||
|
|
||||||
// Pass tool-specific props
|
// Wrap lazy-loaded component with Suspense
|
||||||
switch (selectedToolKey) {
|
return (
|
||||||
case "split":
|
<Suspense fallback={<ToolLoadingFallback toolName={selectedTool.name} />}>
|
||||||
return (
|
<ToolComponent
|
||||||
<ToolComponent
|
onPreviewFile={onPreviewFile}
|
||||||
selectedFiles={toolSelectedFiles}
|
onComplete={onComplete}
|
||||||
onPreviewFile={onPreviewFile}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
);
|
</Suspense>
|
||||||
case "compress":
|
);
|
||||||
return (
|
|
||||||
<ToolComponent
|
|
||||||
files={files}
|
|
||||||
setLoading={(loading: boolean) => {}}
|
|
||||||
params={toolParams}
|
|
||||||
updateParams={updateParams}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "merge":
|
|
||||||
return (
|
|
||||||
<ToolComponent
|
|
||||||
files={files}
|
|
||||||
params={toolParams}
|
|
||||||
updateParams={updateParams}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<ToolComponent
|
|
||||||
files={files}
|
|
||||||
params={toolParams}
|
|
||||||
updateParams={updateParams}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ToolRenderer;
|
export default ToolRenderer;
|
||||||
|
@ -98,7 +98,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
|
|||||||
|
|
||||||
case 'REMOVE_FILES':
|
case 'REMOVE_FILES':
|
||||||
const remainingFiles = state.activeFiles.filter(file => {
|
const remainingFiles = state.activeFiles.filter(file => {
|
||||||
const fileId = (file as any).id || file.name;
|
const fileId = (file as File & { id?: string }).id || file.name;
|
||||||
return !action.payload.includes(fileId);
|
return !action.payload.includes(fileId);
|
||||||
});
|
});
|
||||||
const safeSelectedFileIds = Array.isArray(state.selectedFileIds) ? state.selectedFileIds : [];
|
const safeSelectedFileIds = Array.isArray(state.selectedFileIds) ? state.selectedFileIds : [];
|
||||||
@ -347,7 +347,7 @@ export function FileContextProvider({
|
|||||||
// Cleanup timers and refs
|
// Cleanup timers and refs
|
||||||
const cleanupTimers = useRef<Map<string, NodeJS.Timeout>>(new Map());
|
const cleanupTimers = useRef<Map<string, NodeJS.Timeout>>(new Map());
|
||||||
const blobUrls = useRef<Set<string>>(new Set());
|
const blobUrls = useRef<Set<string>>(new Set());
|
||||||
const pdfDocuments = useRef<Map<string, any>>(new Map());
|
const pdfDocuments = useRef<Map<string, PDFDocument>>(new Map());
|
||||||
|
|
||||||
// Enhanced file processing hook
|
// Enhanced file processing hook
|
||||||
const {
|
const {
|
||||||
@ -381,7 +381,7 @@ export function FileContextProvider({
|
|||||||
blobUrls.current.add(url);
|
blobUrls.current.add(url);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const trackPdfDocument = useCallback((fileId: string, pdfDoc: any) => {
|
const trackPdfDocument = useCallback((fileId: string, pdfDoc: PDFDocument) => {
|
||||||
// Clean up existing document for this file if any
|
// Clean up existing document for this file if any
|
||||||
const existing = pdfDocuments.current.get(fileId);
|
const existing = pdfDocuments.current.get(fileId);
|
||||||
if (existing && existing.destroy) {
|
if (existing && existing.destroy) {
|
||||||
@ -498,7 +498,7 @@ export function FileContextProvider({
|
|||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
try {
|
try {
|
||||||
// Check if file already has an ID (already in IndexedDB)
|
// Check if file already has an ID (already in IndexedDB)
|
||||||
const fileId = (file as any).id;
|
const fileId = (file as File & { id?: string }).id;
|
||||||
if (!fileId) {
|
if (!fileId) {
|
||||||
// File doesn't have ID, store it and get the ID
|
// File doesn't have ID, store it and get the ID
|
||||||
const storedFile = await fileStorage.storeFile(file);
|
const storedFile = await fileStorage.storeFile(file);
|
||||||
@ -680,7 +680,7 @@ export function FileContextProvider({
|
|||||||
// Utility functions
|
// Utility functions
|
||||||
const getFileById = useCallback((fileId: string): File | undefined => {
|
const getFileById = useCallback((fileId: string): File | undefined => {
|
||||||
return state.activeFiles.find(file => {
|
return state.activeFiles.find(file => {
|
||||||
const actualFileId = (file as any).id || file.name;
|
const actualFileId = (file as File & { id?: string }).id || file.name;
|
||||||
return actualFileId === fileId;
|
return actualFileId === fileId;
|
||||||
});
|
});
|
||||||
}, [state.activeFiles]);
|
}, [state.activeFiles]);
|
||||||
|
85
frontend/src/contexts/FileSelectionContext.tsx
Normal file
85
frontend/src/contexts/FileSelectionContext.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
|
||||||
|
import {
|
||||||
|
MaxFiles,
|
||||||
|
FileSelectionState,
|
||||||
|
FileSelectionActions,
|
||||||
|
FileSelectionComputed,
|
||||||
|
FileSelectionContextValue
|
||||||
|
} from '../types/tool';
|
||||||
|
|
||||||
|
interface FileSelectionProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileSelectionContext = createContext<FileSelectionContextValue | undefined>(undefined);
|
||||||
|
|
||||||
|
export function FileSelectionProvider({ children }: FileSelectionProviderProps) {
|
||||||
|
// State
|
||||||
|
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||||
|
const [maxFiles, setMaxFiles] = useState<MaxFiles>(-1); // Default: unlimited
|
||||||
|
const [isToolMode, setIsToolMode] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const clearSelection = useCallback(() => {
|
||||||
|
setSelectedFiles([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Computed properties
|
||||||
|
const selectionCount = selectedFiles.length;
|
||||||
|
const canSelectMore = maxFiles === -1 || selectionCount < maxFiles;
|
||||||
|
const isAtLimit = maxFiles > 0 && selectionCount >= maxFiles;
|
||||||
|
const isMultiFileMode = maxFiles !== 1;
|
||||||
|
|
||||||
|
const contextValue: FileSelectionContextValue = {
|
||||||
|
// State
|
||||||
|
selectedFiles,
|
||||||
|
maxFiles,
|
||||||
|
isToolMode,
|
||||||
|
// Actions
|
||||||
|
setSelectedFiles,
|
||||||
|
setMaxFiles,
|
||||||
|
setIsToolMode,
|
||||||
|
clearSelection,
|
||||||
|
// Computed
|
||||||
|
canSelectMore,
|
||||||
|
isAtLimit,
|
||||||
|
selectionCount,
|
||||||
|
isMultiFileMode
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileSelectionContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</FileSelectionContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom hook to use the context
|
||||||
|
export function useFileSelection(): FileSelectionContextValue {
|
||||||
|
const context = useContext(FileSelectionContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useFileSelection must be used within a FileSelectionProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper hooks for specific use cases with strict typing
|
||||||
|
export function useToolFileSelection(): Pick<FileSelectionContextValue, 'selectedFiles' | 'maxFiles' | 'canSelectMore' | 'isAtLimit' | 'selectionCount'> {
|
||||||
|
const { selectedFiles, maxFiles, canSelectMore, isAtLimit, selectionCount } = useFileSelection();
|
||||||
|
return { selectedFiles, maxFiles, canSelectMore, isAtLimit, selectionCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFileSelectionActions(): Pick<FileSelectionContextValue, 'setSelectedFiles' | 'clearSelection' | 'setMaxFiles' | 'setIsToolMode'> {
|
||||||
|
const { setSelectedFiles, clearSelection, setMaxFiles, setIsToolMode } = useFileSelection();
|
||||||
|
return { setSelectedFiles, clearSelection, setMaxFiles, setIsToolMode };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFileSelectionState(): Pick<FileSelectionContextValue, 'selectedFiles' | 'maxFiles' | 'isToolMode'> {
|
||||||
|
const { selectedFiles, maxFiles, isToolMode } = useFileSelection();
|
||||||
|
return { selectedFiles, maxFiles, isToolMode };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFileSelectionComputed(): Pick<FileSelectionContextValue, 'canSelectMore' | 'isAtLimit' | 'selectionCount' | 'isMultiFileMode'> {
|
||||||
|
const { canSelectMore, isAtLimit, selectionCount, isMultiFileMode } = useFileSelection();
|
||||||
|
return { canSelectMore, isAtLimit, selectionCount, isMultiFileMode };
|
||||||
|
}
|
@ -3,62 +3,88 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import AddToPhotosIcon from "@mui/icons-material/AddToPhotos";
|
import AddToPhotosIcon from "@mui/icons-material/AddToPhotos";
|
||||||
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
||||||
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
||||||
import SplitPdfPanel from "../tools/Split";
|
|
||||||
import CompressPdfPanel from "../tools/Compress";
|
|
||||||
import MergePdfPanel from "../tools/Merge";
|
|
||||||
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
||||||
|
import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool";
|
||||||
|
|
||||||
type ToolRegistryEntry = {
|
// Import types from central location - no need to redefine
|
||||||
icon: React.ReactNode;
|
|
||||||
name: string;
|
|
||||||
component: React.ComponentType<any>;
|
|
||||||
view: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ToolRegistry = {
|
// Tool definitions using simplified interface
|
||||||
[key: string]: ToolRegistryEntry;
|
// IMPORTANT: Adding a new tool is just 2 steps:
|
||||||
};
|
// 1. Add entry here with maxFiles, endpoints, and lazy component
|
||||||
|
// 2. Create the tool component - NO HomePage changes needed!
|
||||||
const baseToolRegistry = {
|
// The system automatically handles FileEditor, file selection, and rendering
|
||||||
split: { icon: <ContentCutIcon />, component: SplitPdfPanel, view: "split" },
|
const toolDefinitions: Record<string, ToolDefinition> = {
|
||||||
compress: { icon: <ZoomInMapIcon />, component: CompressPdfPanel, view: "viewer" },
|
split: {
|
||||||
merge: { icon: <AddToPhotosIcon />, component: MergePdfPanel, view: "pageEditor" },
|
id: "split",
|
||||||
};
|
icon: <ContentCutIcon />,
|
||||||
|
component: React.lazy(() => import("../tools/Split")),
|
||||||
// Tool endpoint mappings
|
maxFiles: 1,
|
||||||
const toolEndpoints: Record<string, string[]> = {
|
category: "manipulation",
|
||||||
split: ["split-pages", "split-pdf-by-sections", "split-by-size-or-count", "split-pdf-by-chapters"],
|
description: "Split PDF files into smaller parts",
|
||||||
compress: ["compress-pdf"],
|
endpoints: ["split-pages", "split-pdf-by-sections", "split-by-size-or-count", "split-pdf-by-chapters"]
|
||||||
merge: ["merge-pdfs"],
|
},
|
||||||
|
// compress and merge are disabled for now - legacy tools to be overhauled
|
||||||
|
// compress: {
|
||||||
|
// id: "compress",
|
||||||
|
// icon: <ZoomInMapIcon />,
|
||||||
|
// component: React.lazy(() => import("../tools/Compress")),
|
||||||
|
// maxFiles: 1,
|
||||||
|
// category: "optimization",
|
||||||
|
// description: "Reduce PDF file size",
|
||||||
|
// endpoints: ["compress-pdf"]
|
||||||
|
// },
|
||||||
|
// merge: {
|
||||||
|
// id: "merge",
|
||||||
|
// icon: <AddToPhotosIcon />,
|
||||||
|
// component: React.lazy(() => import("../tools/Merge")),
|
||||||
|
// maxFiles: -1,
|
||||||
|
// category: "manipulation",
|
||||||
|
// description: "Combine multiple PDF files",
|
||||||
|
// endpoints: ["merge-pdfs"]
|
||||||
|
// },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const useToolManagement = () => {
|
interface ToolManagementResult {
|
||||||
|
selectedToolKey: string | null;
|
||||||
|
selectedTool: Tool | null;
|
||||||
|
toolSelectedFileIds: string[];
|
||||||
|
toolRegistry: ToolRegistry;
|
||||||
|
selectTool: (toolKey: string) => void;
|
||||||
|
clearToolSelection: () => void;
|
||||||
|
setToolSelectedFileIds: (fileIds: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useToolManagement = (): ToolManagementResult => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [selectedToolKey, setSelectedToolKey] = useState<string | null>(null);
|
const [selectedToolKey, setSelectedToolKey] = useState<string | null>(null);
|
||||||
const [toolSelectedFileIds, setToolSelectedFileIds] = useState<string[]>([]);
|
const [toolSelectedFileIds, setToolSelectedFileIds] = useState<string[]>([]);
|
||||||
|
|
||||||
const allEndpoints = Array.from(new Set(Object.values(toolEndpoints).flat()));
|
const allEndpoints = Array.from(new Set(
|
||||||
|
Object.values(toolDefinitions).flatMap(tool => tool.endpoints || [])
|
||||||
|
));
|
||||||
const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
|
const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
|
||||||
|
|
||||||
const isToolAvailable = useCallback((toolKey: string): boolean => {
|
const isToolAvailable = useCallback((toolKey: string): boolean => {
|
||||||
if (endpointsLoading) return true;
|
if (endpointsLoading) return true;
|
||||||
const endpoints = toolEndpoints[toolKey] || [];
|
const tool = toolDefinitions[toolKey];
|
||||||
return endpoints.some(endpoint => endpointStatus[endpoint] === true);
|
if (!tool?.endpoints) return true;
|
||||||
|
return tool.endpoints.some(endpoint => endpointStatus[endpoint] === true);
|
||||||
}, [endpointsLoading, endpointStatus]);
|
}, [endpointsLoading, endpointStatus]);
|
||||||
|
|
||||||
const toolRegistry: ToolRegistry = useMemo(() => {
|
const toolRegistry: ToolRegistry = useMemo(() => {
|
||||||
const availableToolRegistry: ToolRegistry = {};
|
const availableTools: ToolRegistry = {};
|
||||||
Object.keys(baseToolRegistry).forEach(toolKey => {
|
Object.keys(toolDefinitions).forEach(toolKey => {
|
||||||
if (isToolAvailable(toolKey)) {
|
if (isToolAvailable(toolKey)) {
|
||||||
availableToolRegistry[toolKey] = {
|
const toolDef = toolDefinitions[toolKey];
|
||||||
...baseToolRegistry[toolKey as keyof typeof baseToolRegistry],
|
availableTools[toolKey] = {
|
||||||
|
...toolDef,
|
||||||
name: t(`home.${toolKey}.title`, toolKey.charAt(0).toUpperCase() + toolKey.slice(1))
|
name: t(`home.${toolKey}.title`, toolKey.charAt(0).toUpperCase() + toolKey.slice(1))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return availableToolRegistry;
|
return availableTools;
|
||||||
}, [t, isToolAvailable]);
|
}, [t, isToolAvailable]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useCallback} from "react";
|
import React, { useState, useCallback, useEffect} from "react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useFileContext } from "../contexts/FileContext";
|
import { useFileContext } from "../contexts/FileContext";
|
||||||
|
import { FileSelectionProvider, useFileSelection } from "../contexts/FileSelectionContext";
|
||||||
import { useToolManagement } from "../hooks/useToolManagement";
|
import { useToolManagement } from "../hooks/useToolManagement";
|
||||||
import { Group, Box, Button, Container } from "@mantine/core";
|
import { Group, Box, Button, Container } from "@mantine/core";
|
||||||
import { useRainbowThemeContext } from "../components/shared/RainbowThemeProvider";
|
import { useRainbowThemeContext } from "../components/shared/RainbowThemeProvider";
|
||||||
@ -17,7 +18,8 @@ import ToolRenderer from "../components/tools/ToolRenderer";
|
|||||||
import QuickAccessBar from "../components/shared/QuickAccessBar";
|
import QuickAccessBar from "../components/shared/QuickAccessBar";
|
||||||
import { useMultipleEndpointsEnabled } from "../hooks/useEndpointConfig";
|
import { useMultipleEndpointsEnabled } from "../hooks/useEndpointConfig";
|
||||||
|
|
||||||
export default function HomePage() {
|
// Inner component that uses file selection context
|
||||||
|
function HomePageContent() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
|
|
||||||
@ -25,35 +27,60 @@ export default function HomePage() {
|
|||||||
const fileContext = useFileContext();
|
const fileContext = useFileContext();
|
||||||
const { activeFiles, currentView, currentMode, setCurrentView, addFiles } = fileContext;
|
const { activeFiles, currentView, currentMode, setCurrentView, addFiles } = fileContext;
|
||||||
|
|
||||||
|
// Get file selection context
|
||||||
|
const { setMaxFiles, setIsToolMode, setSelectedFiles } = useFileSelection();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedToolKey,
|
selectedToolKey,
|
||||||
selectedTool,
|
selectedTool,
|
||||||
toolParams,
|
toolSelectedFileIds,
|
||||||
toolRegistry,
|
toolRegistry,
|
||||||
selectTool,
|
selectTool,
|
||||||
clearToolSelection,
|
clearToolSelection,
|
||||||
updateToolParams,
|
setToolSelectedFileIds,
|
||||||
} = useToolManagement();
|
} = useToolManagement();
|
||||||
|
|
||||||
const [toolSelectedFiles, setToolSelectedFiles] = useState<File[]>([]);
|
|
||||||
const [sidebarsVisible, setSidebarsVisible] = useState(true);
|
const [sidebarsVisible, setSidebarsVisible] = useState(true);
|
||||||
const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker');
|
const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker');
|
||||||
const [readerMode, setReaderMode] = useState(false);
|
const [readerMode, setReaderMode] = useState(false);
|
||||||
const [pageEditorFunctions, setPageEditorFunctions] = useState<any>(null);
|
const [pageEditorFunctions, setPageEditorFunctions] = useState<{
|
||||||
|
closePdf: () => void;
|
||||||
|
handleUndo: () => void;
|
||||||
|
handleRedo: () => void;
|
||||||
|
canUndo: boolean;
|
||||||
|
canRedo: boolean;
|
||||||
|
handleRotate: () => void;
|
||||||
|
handleDelete: () => void;
|
||||||
|
handleSplit: () => void;
|
||||||
|
onExportSelected: () => void;
|
||||||
|
onExportAll: () => void;
|
||||||
|
exportLoading: boolean;
|
||||||
|
selectionMode: boolean;
|
||||||
|
selectedPages: number[];
|
||||||
|
} | null>(null);
|
||||||
const [previewFile, setPreviewFile] = useState<File | null>(null);
|
const [previewFile, setPreviewFile] = useState<File | null>(null);
|
||||||
|
|
||||||
|
// Update file selection context when tool changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedTool) {
|
||||||
|
setMaxFiles(selectedTool.maxFiles);
|
||||||
|
setIsToolMode(true);
|
||||||
|
} else {
|
||||||
|
setMaxFiles(-1); // Unlimited when not in tool mode
|
||||||
|
setIsToolMode(false);
|
||||||
|
setSelectedFiles([]); // Clear selection when exiting tool mode
|
||||||
|
}
|
||||||
|
}, [selectedTool, setMaxFiles, setIsToolMode, setSelectedFiles]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleToolSelect = useCallback(
|
const handleToolSelect = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
selectTool(id);
|
selectTool(id);
|
||||||
if (toolRegistry[id]?.view) setCurrentView(toolRegistry[id].view);
|
setCurrentView('fileEditor'); // Tools use fileEditor view for file selection
|
||||||
setLeftPanelView('toolContent');
|
setLeftPanelView('toolContent');
|
||||||
setReaderMode(false);
|
setReaderMode(false);
|
||||||
},
|
},
|
||||||
[selectTool, toolRegistry, setCurrentView]
|
[selectTool, setCurrentView]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleQuickAccessTools = useCallback(() => {
|
const handleQuickAccessTools = useCallback(() => {
|
||||||
@ -145,7 +172,6 @@ export default function HomePage() {
|
|||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
<ToolRenderer
|
<ToolRenderer
|
||||||
selectedToolKey={selectedToolKey}
|
selectedToolKey={selectedToolKey}
|
||||||
toolSelectedFiles={toolSelectedFiles}
|
|
||||||
onPreviewFile={setPreviewFile}
|
onPreviewFile={setPreviewFile}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -194,7 +220,15 @@ export default function HomePage() {
|
|||||||
maxRecentFiles={8}
|
maxRecentFiles={8}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
|
) : currentView === "fileEditor" && selectedToolKey ? (
|
||||||
|
// Tool-specific FileEditor - for file selection with tools
|
||||||
|
<FileEditor
|
||||||
|
toolMode={true}
|
||||||
|
showUpload={true}
|
||||||
|
showBulkActions={false}
|
||||||
|
/>
|
||||||
) : currentView === "fileEditor" ? (
|
) : currentView === "fileEditor" ? (
|
||||||
|
// Generic FileEditor - for general file management
|
||||||
<FileEditor
|
<FileEditor
|
||||||
onOpenPageEditor={(file) => {
|
onOpenPageEditor={(file) => {
|
||||||
handleViewChange("pageEditor");
|
handleViewChange("pageEditor");
|
||||||
@ -248,17 +282,8 @@ export default function HomePage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : currentView === "split" ? (
|
|
||||||
<FileEditor
|
|
||||||
toolMode={true}
|
|
||||||
multiSelect={false}
|
|
||||||
showUpload={true}
|
|
||||||
showBulkActions={true}
|
|
||||||
onFileSelect={(files) => {
|
|
||||||
setToolSelectedFiles(files);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : selectedToolKey && selectedTool ? (
|
) : selectedToolKey && selectedTool ? (
|
||||||
|
// Fallback: if tool is selected but not in fileEditor view, show tool in main area
|
||||||
<ToolRenderer
|
<ToolRenderer
|
||||||
selectedToolKey={selectedToolKey}
|
selectedToolKey={selectedToolKey}
|
||||||
/>
|
/>
|
||||||
@ -285,3 +310,12 @@ export default function HomePage() {
|
|||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Main HomePage component wrapped with FileSelectionProvider
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<FileSelectionProvider>
|
||||||
|
<HomePageContent />
|
||||||
|
</FileSelectionProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -4,26 +4,23 @@ import { useTranslation } from "react-i18next";
|
|||||||
import DownloadIcon from "@mui/icons-material/Download";
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
import { useFileContext } from "../contexts/FileContext";
|
import { useFileContext } from "../contexts/FileContext";
|
||||||
|
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||||
|
|
||||||
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||||
import OperationButton from "../components/tools/shared/OperationButton";
|
import OperationButton from "../components/tools/shared/OperationButton";
|
||||||
import ErrorNotification from "../components/tools/shared/ErrorNotification";
|
import ErrorNotification from "../components/tools/shared/ErrorNotification";
|
||||||
import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator";
|
import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator";
|
||||||
import ResultsPreview from "../components/tools/shared/ResultsPreview";
|
import ResultsPreview from "../components/tools/shared/ResultsPreview";
|
||||||
|
|
||||||
import SplitSettings from "../components/tools/split/SplitSettings";
|
import SplitSettings from "../components/tools/split/SplitSettings";
|
||||||
|
|
||||||
import { useSplitParameters } from "../hooks/tools/split/useSplitParameters";
|
import { useSplitParameters } from "../hooks/tools/split/useSplitParameters";
|
||||||
import { useSplitOperation } from "../hooks/tools/split/useSplitOperation";
|
import { useSplitOperation } from "../hooks/tools/split/useSplitOperation";
|
||||||
|
import { BaseToolProps } from "../types/tool";
|
||||||
|
|
||||||
interface SplitProps {
|
const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
selectedFiles?: File[];
|
|
||||||
onPreviewFile?: (file: File | null) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Split = ({ selectedFiles = [], onPreviewFile }: SplitProps) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setCurrentMode } = useFileContext();
|
const { setCurrentMode } = useFileContext();
|
||||||
|
const { selectedFiles } = useToolFileSelection();
|
||||||
|
|
||||||
const splitParams = useSplitParameters();
|
const splitParams = useSplitParameters();
|
||||||
const splitOperation = useSplitOperation();
|
const splitOperation = useSplitOperation();
|
||||||
@ -39,11 +36,20 @@ const Split = ({ selectedFiles = [], onPreviewFile }: SplitProps) => {
|
|||||||
}, [splitParams.mode, splitParams.parameters, selectedFiles]);
|
}, [splitParams.mode, splitParams.parameters, selectedFiles]);
|
||||||
|
|
||||||
const handleSplit = async () => {
|
const handleSplit = async () => {
|
||||||
await splitOperation.executeOperation(
|
try {
|
||||||
splitParams.mode,
|
await splitOperation.executeOperation(
|
||||||
splitParams.parameters,
|
splitParams.mode,
|
||||||
selectedFiles
|
splitParams.parameters,
|
||||||
);
|
selectedFiles
|
||||||
|
);
|
||||||
|
if (splitOperation.files && onComplete) {
|
||||||
|
onComplete(splitOperation.files);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (onError) {
|
||||||
|
onError(error instanceof Error ? error.message : 'Split operation failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleThumbnailClick = (file: File) => {
|
const handleThumbnailClick = (file: File) => {
|
||||||
|
100
frontend/src/types/tool.ts
Normal file
100
frontend/src/types/tool.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// Type definitions for better type safety
|
||||||
|
export type MaxFiles = number; // 1 = single file, >1 = limited multi-file, -1 = unlimited
|
||||||
|
export type ToolCategory = 'manipulation' | 'conversion' | 'analysis' | 'utility' | 'optimization' | 'security';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool definition without name - used for base definitions before translation
|
||||||
|
*/
|
||||||
|
export type ToolDefinition = Omit<Tool, 'name'>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard interface that all modern tools should implement
|
||||||
|
* This ensures consistent behavior and makes adding new tools trivial
|
||||||
|
*/
|
||||||
|
export interface BaseToolProps {
|
||||||
|
// Tool results callback - called when tool completes successfully
|
||||||
|
onComplete?: (results: File[]) => void;
|
||||||
|
|
||||||
|
// Error handling callback
|
||||||
|
onError?: (error: string) => void;
|
||||||
|
|
||||||
|
// Preview functionality for result files
|
||||||
|
onPreviewFile?: (file: File | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool step types for standardized UI
|
||||||
|
*/
|
||||||
|
export type ToolStepType = 'files' | 'settings' | 'results';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool step configuration
|
||||||
|
*/
|
||||||
|
export interface ToolStepConfig {
|
||||||
|
type: ToolStepType;
|
||||||
|
title: string;
|
||||||
|
isVisible: boolean;
|
||||||
|
isCompleted: boolean;
|
||||||
|
isCollapsed?: boolean;
|
||||||
|
completedMessage?: string;
|
||||||
|
onCollapsedClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool operation result
|
||||||
|
*/
|
||||||
|
export interface ToolResult {
|
||||||
|
success: boolean;
|
||||||
|
files?: File[];
|
||||||
|
error?: string;
|
||||||
|
downloadUrl?: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete tool definition - single interface for all tool needs
|
||||||
|
*/
|
||||||
|
export interface Tool {
|
||||||
|
id: string;
|
||||||
|
name: string; // Always required - added via translation
|
||||||
|
icon: React.ReactNode; // Always required - for UI display
|
||||||
|
component: React.ComponentType<BaseToolProps>; // Lazy-loaded tool component
|
||||||
|
maxFiles: MaxFiles; // File selection limit: 1=single, 5=limited, -1=unlimited
|
||||||
|
category?: ToolCategory; // Tool grouping for organization
|
||||||
|
description?: string; // Help text for users
|
||||||
|
endpoints?: string[]; // Backend endpoints this tool uses
|
||||||
|
supportedFormats?: string[]; // File types this tool accepts
|
||||||
|
validation?: (files: File[]) => { valid: boolean; message?: string }; // File validation logic
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool registry type - tools indexed by key
|
||||||
|
*/
|
||||||
|
export type ToolRegistry = Record<string, Tool>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File selection context interfaces for type safety
|
||||||
|
*/
|
||||||
|
export interface FileSelectionState {
|
||||||
|
selectedFiles: File[];
|
||||||
|
maxFiles: MaxFiles;
|
||||||
|
isToolMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileSelectionActions {
|
||||||
|
setSelectedFiles: (files: File[]) => void;
|
||||||
|
setMaxFiles: (maxFiles: MaxFiles) => void;
|
||||||
|
setIsToolMode: (isToolMode: boolean) => void;
|
||||||
|
clearSelection: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileSelectionComputed {
|
||||||
|
canSelectMore: boolean;
|
||||||
|
isAtLimit: boolean;
|
||||||
|
selectionCount: number;
|
||||||
|
isMultiFileMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileSelectionContextValue extends FileSelectionState, FileSelectionActions, FileSelectionComputed {}
|
Loading…
Reference in New Issue
Block a user