import { useState, useEffect, useCallback } from 'react'; import api from '../api'; import { useToast } from '../contexts/ToastContext'; import { useLanguage } from '../providers/language/LanguageContext'; import { getUserDataCache } from '../utils/userCache'; import { useApiRequest } from './useApi'; import { usePermissions, type UserPermissions } from './usePermissions'; import { fetchFileAttributes as _fetchFileAttributes, fetchFiles as fetchFilesApi, fetchFileById as fetchFileByIdApi, updateFile as updateFileApi, deleteFile as deleteFileApi, deleteFiles as deleteFilesApi } from '../api/fileApi'; // File interfaces - exactly matching backend FileItem model export interface FileInfo { id: string; mandateId: string; // Required in backend fileName: string; // Required in backend mimeType: string; fileHash: string; fileSize: number; creationDate: number; // Backend uses float for UTC timestamp in seconds } // UserFile is now just the backend response - no mapping needed // Field names come directly from backend attributes export type UserFile = any; // Attribute definition interface (local definition, not imported to avoid conflicts) export interface AttributeDefinition { name: string; label: string; type: 'string' | 'number' | 'date' | 'boolean' | 'enum'; sortable?: boolean; filterable?: boolean; searchable?: boolean; width?: number; minWidth?: number; maxWidth?: number; filterOptions?: string[]; // For enum types } // Pagination parameters (local definition, not imported to avoid conflicts) export interface PaginationParams { page?: number; pageSize?: number; sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; } // Files list hook export function useUserFiles() { const [files, setFiles] = useState([]); const [attributes, setAttributes] = useState([]); const [permissions, setPermissions] = useState(null); const [pagination, setPagination] = useState<{ currentPage: number; pageSize: number; totalItems: number; totalPages: number; } | null>(null); const { request, isLoading: loading, error } = useApiRequest(); const { checkPermission } = usePermissions(); // Fetch attributes from backend const fetchAttributes = useCallback(async () => { try { // Note: fetchFileAttributes uses api.get directly due to response format handling // For now, we'll use api.get directly here as well const response = await api.get('/api/attributes/FileItem'); // Extract attributes from response - check if response.data.attributes exists, otherwise check if response.data is an array let attrs: AttributeDefinition[] = []; if (response.data?.attributes && Array.isArray(response.data.attributes)) { attrs = response.data.attributes; } else if (Array.isArray(response.data)) { attrs = response.data; } else if (response.data && typeof response.data === 'object') { // Try to find any array property in the response const keys = Object.keys(response.data); for (const key of keys) { if (Array.isArray(response.data[key])) { attrs = response.data[key]; break; } } } setAttributes(attrs); return attrs; } catch (error: any) { console.error('Error fetching attributes:', error); setAttributes([]); return []; } }, []); // Fetch permissions from backend const fetchPermissions = useCallback(async () => { try { const perms = await checkPermission('DATA', 'FileItem'); setPermissions(perms); return perms; } catch (error: any) { console.error('Error fetching permissions:', error); const defaultPerms: UserPermissions = { view: false, read: 'n', create: 'n', update: 'n', delete: 'n', }; setPermissions(defaultPerms); return defaultPerms; } }, [checkPermission]); const fetchFiles = useCallback(async (params?: PaginationParams) => { // Check if user is authenticated before fetching files const cachedUser = getUserDataCache(); if (!cachedUser) { // User is not authenticated, skip fetching files setFiles([]); // Note: loading and error are managed by useApiRequest hook return; } try { const data = await fetchFilesApi(request, params); // Log the entire backend response for debugging console.log('📦 Backend response from /api/files/list:', { fullResponse: data, responseType: typeof data, isArray: Array.isArray(data), hasItems: data && typeof data === 'object' && 'items' in data, itemsCount: data && typeof data === 'object' && 'items' in data ? (Array.isArray(data.items) ? data.items.length : 'not an array') : 'no items property', firstItem: data && typeof data === 'object' && 'items' in data && Array.isArray(data.items) && data.items.length > 0 ? data.items[0] : 'no first item', pagination: data && typeof data === 'object' && 'pagination' in data ? data.pagination : 'no pagination' }); // Handle paginated response if (data && typeof data === 'object' && 'items' in data) { const items = Array.isArray(data.items) ? data.items : []; console.log('📋 Processing paginated response:', { itemsCount: items.length, firstItemRaw: items.length > 0 ? items[0] : null, allItemsRaw: items }); // Use backend data directly - no mapping needed, just like prompts // Field names come from backend attributes console.log('📊 Final files array (using backend data directly):', items); setFiles(items); if (data.pagination) { setPagination(data.pagination); } } else { // Handle non-paginated response (backward compatibility) console.log('📋 Processing non-paginated response:', { isArray: Array.isArray(data), dataLength: Array.isArray(data) ? data.length : 'not an array', firstItemRaw: Array.isArray(data) && data.length > 0 ? data[0] : null, allDataRaw: data }); // Use backend data directly - no mapping needed, just like prompts const items = Array.isArray(data) ? data : []; console.log('📊 Final files array (non-paginated, using backend data directly):', items); setFiles(items); setPagination(null); } } catch (error: any) { // Error is already handled by useApiRequest setFiles([]); setPagination(null); } }, [request]); // Optimistically remove a file from the local state const removeFileOptimistically = (fileId: string) => { setFiles(prevFiles => prevFiles.filter(file => file.id !== fileId)); }; // Optimistically update a file in the local state const updateFileOptimistically = (fileId: string, updateData: any) => { setFiles(prevFiles => prevFiles.map(file => file.id === fileId ? { ...file, ...updateData } : file ) ); }; // Fetch a single file by ID const fetchFileById = useCallback(async (fileId: string): Promise => { return await fetchFileByIdApi(request, fileId); }, [request]); // Generate edit fields from attributes dynamically const generateEditFieldsFromAttributes = useCallback((): Array<{ key: string; label: string; type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'readonly'; editable?: boolean; required?: boolean; validator?: (value: any) => string | null; minRows?: number; maxRows?: number; }> => { if (!attributes || attributes.length === 0) { return []; } const editableFields = attributes .filter(attr => { // Filter out non-editable fields (id, mandateId, fileHash, etc.) const nonEditableFields = ['id', 'mandateId', 'fileHash', 'creationDate', 'source']; return !nonEditableFields.includes(attr.name); }) .map(attr => { // Map attribute type to form field type let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'readonly' = 'string'; if (attr.type === 'boolean') { fieldType = 'boolean'; } else if (attr.type === 'date') { fieldType = 'date'; } else if (attr.type === 'enum' && attr.filterOptions) { fieldType = 'enum'; } else if (attr.name === 'fileName' || attr.name === 'file_name') { fieldType = 'string'; } // Define validators and required fields let required = false; let validator: ((value: any) => string | null) | undefined = undefined; if (attr.name === 'fileName' || attr.name === 'file_name') { required = true; validator = (value: any) => { if (!value || (typeof value === 'string' && value.trim() === '')) { return 'File name cannot be empty'; } if (typeof value === 'string' && value.length > 255) { return 'File name cannot exceed 255 characters'; } return null; }; } return { key: attr.name, label: attr.label || attr.name, type: fieldType, editable: true, required, validator }; }); return editableFields; }, [attributes]); // Ensure attributes are loaded - can be called by EditActionButton const ensureAttributesLoaded = useCallback(async () => { // If attributes are already loaded, return them if (attributes && attributes.length > 0) { return attributes; } // Otherwise, fetch them and return the result const fetchedAttributes = await fetchAttributes(); return fetchedAttributes; }, [attributes, fetchAttributes]); // Fetch attributes and permissions on mount // Note: Do NOT fetch files here - let the table component control pagination useEffect(() => { fetchAttributes(); fetchPermissions(); }, [fetchAttributes, fetchPermissions]); // Listen for file upload events and refresh the list useEffect(() => { const handleFileUploaded = (event: CustomEvent) => { console.log('📁 File uploaded event received, refreshing list...', event.detail); // Small delay to ensure backend has persisted the file setTimeout(() => { fetchFiles(); }, 100); }; window.addEventListener('fileUploaded', handleFileUploaded as EventListener); return () => { window.removeEventListener('fileUploaded', handleFileUploaded as EventListener); }; }, [fetchFiles]); return { data: files, loading, error, refetch: fetchFiles, removeFileOptimistically, updateFileOptimistically, attributes, permissions, pagination, fetchFileById, generateEditFieldsFromAttributes, ensureAttributesLoaded }; } // File operations hook export function useFileOperations() { const [downloadingFiles, setDownloadingFiles] = useState>(new Set()); const [deletingFiles, setDeletingFiles] = useState>(new Set()); const [editingFiles, setEditingFiles] = useState>(new Set()); const [uploadingFile, setUploadingFile] = useState(false); const [isLoading] = useState(false); const [downloadError, setDownloadError] = useState(null); const [deleteError, setDeleteError] = useState(null); const [uploadError, setUploadError] = useState(null); const [previewingFiles, setPreviewingFiles] = useState>(new Set()); const [previewError, setPreviewError] = useState(null); // Toast for notifications const { showWarning } = useToast(); // Language context const { t } = useLanguage(); // API request function for API layer calls const { request } = useApiRequest(); const handleFileDownload = async (fileId: string, fileName: string) => { setDownloadError(null); setDownloadingFiles(prev => new Set(prev).add(fileId)); try { // Try to get the file download const response = await api.get(`/api/files/${fileId}/download`, { responseType: 'blob', validateStatus: function (status: number) { return status >= 200 && status < 300; // default } }); const blob = response.data; // Create a download link and trigger the download const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.setAttribute('download', fileName); document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); return true; } catch (error: any) { console.error(`❌ Download failed for ${fileName}:`, error); let errorMessage = error.message; if (error.response?.status === 404) { errorMessage = `File "${fileName}" not found or has been deleted.`; } else if (error.response?.status === 403) { errorMessage = `No permission to download "${fileName}".`; } setDownloadError(errorMessage); return false; } finally { setDownloadingFiles(prev => { const newSet = new Set(prev); newSet.delete(fileId); return newSet; }); } }; const handleFileDelete = async (fileId: string, onOptimisticDelete?: () => void) => { setDeleteError(null); setDeletingFiles(prev => new Set(prev).add(fileId)); // Optimistically remove from UI if callback provided if (onOptimisticDelete) { onOptimisticDelete(); } try { await deleteFileApi(request, fileId); // Add a small delay to ensure backend has time to process await new Promise(resolve => setTimeout(resolve, 300)); return true; } catch (error: any) { console.error(`❌ Delete failed for file ID ${fileId}:`, error); let errorMessage = error.message; if (error.response?.status === 404) { errorMessage = `File not found or has already been deleted.`; // If file doesn't exist, consider it successfully "deleted" return true; } else if (error.response?.status === 403) { errorMessage = `No permission to delete this file.`; } setDeleteError(errorMessage); // If deletion failed and we optimistically removed it, we should refetch to restore the file return false; } finally { setDeletingFiles(prev => { const newSet = new Set(prev); newSet.delete(fileId); return newSet; }); } }; const handleFileDeleteMultiple = async (fileIds: string[], onOptimisticDelete?: (fileIds: string[]) => void) => { setDeleteError(null); setDeletingFiles(prev => { const newSet = new Set(prev); fileIds.forEach(id => newSet.add(id)); return newSet; }); // Optimistically remove from UI if callback provided if (onOptimisticDelete) { onOptimisticDelete(fileIds); } try { // Use API function for bulk delete const results = await deleteFilesApi(request, fileIds); // Check if any deletions failed const failures = results.filter(result => !result.success); if (failures.length > 0) { console.error(`❌ ${failures.length} out of ${fileIds.length} files failed to delete`); // For now, we'll consider it successful if at least some files were deleted // In a more robust implementation, you might want to handle partial failures differently } // Add a small delay to ensure backend has time to process await new Promise(resolve => setTimeout(resolve, 300)); return true; } catch (error: any) { console.error(`❌ Bulk delete failed:`, error); setDeleteError(error.message || 'Bulk delete failed'); return false; } finally { setDeletingFiles(prev => { const newSet = new Set(prev); fileIds.forEach(id => newSet.delete(id)); return newSet; }); } }; /** * File upload function - backend bug has been fixed! * * BACKEND FIXES APPLIED: * - Fixed file.fileName → file.filename in routeDataFiles.py * - Removed workflowId from FileItem creation in interfaceComponentObjects.py * - Upload should now work correctly */ const handleFileUpload = async (file: globalThis.File, workflowId?: string, featureInstanceId?: string) => { setUploadError(null); setUploadingFile(true); try { // Validate file before upload if (!file || !file.name || file.name.trim() === '') { throw new Error('Invalid file: File must have a valid name'); } if (file.size === 0) { throw new Error('Invalid file: File cannot be empty'); } const formData = new FormData(); formData.append('file', file); if (workflowId) { formData.append('workflowId', workflowId); } if (featureInstanceId) { formData.append('featureInstanceId', featureInstanceId); } // FormData is now correctly configured for backend const response = await api.post('/api/files/upload', formData, { headers: { 'Content-Type': 'multipart/form-data', } }); const fileData = response.data; // Check if the response indicates a duplicate file if (fileData && fileData.isDuplicate && fileData.message) { const fileName = fileData.originalFileName || file.name; const messageTemplate = t('warning.duplicate_file.message'); const message = messageTemplate.replace('{fileName}', fileName); showWarning(t('warning.duplicate_file.title'), message); } // Dispatch event to notify other components about the new file window.dispatchEvent(new CustomEvent('fileUploaded', { detail: fileData })); return { success: true, fileData }; } catch (error: any) { console.error('Upload failed:', error); let errorMessage = error.message; if (error.response?.status === 500) { if (errorMessage.includes('validation')) { errorMessage = 'File validation failed. Please ensure the file is valid and try again.'; } else { errorMessage = 'Server error during upload. Please try again later.'; } } setUploadError(errorMessage); return { success: false, error: errorMessage }; } finally { setUploadingFile(false); } }; const handleFileUpdate = async (fileId: string, updateData: Partial<{ fileName: string }>, originalFileData?: any) => { setUploadError(null); // Reuse upload error state for update operations setEditingFiles(prev => new Set(prev).add(fileId)); try { // Use PUT request with complete file object // Always use current timestamp for creationDate to avoid validation issues const currentTimestamp = Math.floor(Date.now() / 1000); const creationDate = currentTimestamp; const completeFileObject = { id: fileId, mandateId: originalFileData?.mandateId || "00000000-0000-0000-0000-000000000000", fileName: updateData.fileName, mimeType: originalFileData?.mime_type || "application/octet-stream", fileHash: originalFileData?.fileHash || "0000000000000000000000000000000000000000", fileSize: originalFileData?.size || 0, creationDate: Math.floor(creationDate) // Ensure it's an integer }; const updatedFile = await updateFileApi(request, fileId, completeFileObject); return { success: true, fileData: updatedFile }; } catch (error: any) { console.error(`Update failed for file ID ${fileId}:`, error); let errorMessage = error.message; if (error.response?.status === 404) { errorMessage = `File not found or has been deleted.`; } else if (error.response?.status === 403) { errorMessage = `No permission to update this file.`; } else if (error.response?.status === 400) { errorMessage = `Invalid file update data: ${error.response?.data?.detail || error.response?.data || errorMessage}`; } else if (error.response?.status === 422) { errorMessage = `Validation error: ${error.response?.data?.detail || errorMessage}`; } else if (error.response?.status === 500) { errorMessage = `Server error: ${error.response?.data?.detail || errorMessage}`; } setUploadError(errorMessage); return { success: false, error: errorMessage }; } finally { setEditingFiles(prev => { const newSet = new Set(prev); newSet.delete(fileId); return newSet; }); } }; const handleFilePreview = async (fileId: string, fileName: string, mimeType?: string) => { setPreviewError(null); setPreviewingFiles(prev => new Set(prev).add(fileId)); try { // For PDF files, try JSON response first (API returns base64-encoded PDF) if (mimeType === 'application/pdf') { try { const response = await api.get(`/api/files/${fileId}/preview`, { responseType: 'json', validateStatus: function (status: number) { return status >= 200 && status < 300; } }); const jsonResponse = response.data; // Check if response has base64-encoded PDF content if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) { let content = jsonResponse.content; // The content field contains base64-encoded JSON, so decode it first if (typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) { try { const decodedJsonString = atob(content); // Parse the decoded JSON string const nestedJson = JSON.parse(decodedJsonString); if (nestedJson && typeof nestedJson === 'object' && 'content' in nestedJson) { const innerContent = nestedJson.content; const isBase64 = /^[A-Za-z0-9+/=]+$/.test(innerContent); if (isBase64) { // It's base64-encoded PDF content content = innerContent; } else { // It's plain text content, not a PDF // Return the text content for the FilePreview to handle as text return { success: true, previewUrl: null, blob: null, isJsonContent: true, decodedContent: innerContent, isTextContent: true }; } } } catch (decodeError) { console.warn('⚠️ Failed to decode base64 content or parse JSON:', decodeError); } } // Decode base64 content let decodedContent; try { decodedContent = atob(content); // Verify it's actually a PDF const isPDF = decodedContent.startsWith('%PDF'); if (!isPDF) { console.warn('⚠️ Decoded content does not appear to be a valid PDF'); } } catch (decodeError) { console.error('❌ Failed to decode base64 PDF content:', decodeError); throw new Error('Failed to decode PDF content'); } // Create a blob from the decoded PDF content // Convert string to Uint8Array for proper binary handling const uint8Array = new Uint8Array(decodedContent.length); for (let i = 0; i < decodedContent.length; i++) { uint8Array[i] = decodedContent.charCodeAt(i); } const blob = new Blob([uint8Array], { type: 'application/pdf' }); const url = window.URL.createObjectURL(blob); return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent }; } else { throw new Error('No content field in PDF response'); } } catch (jsonError) { // Fallback to blob response const response = await api.get(`/api/files/${fileId}/preview`, { responseType: 'blob', validateStatus: function (status: number) { return status >= 200 && status < 300; } }); const previewData = response.data; const url = window.URL.createObjectURL(previewData); return { success: true, previewUrl: url, blob: previewData, isJsonContent: false }; } } // For image files, try JSON response first (API returns base64-encoded images) if (mimeType?.startsWith('image/')) { try { const response = await api.get(`/api/files/${fileId}/preview`, { responseType: 'json', validateStatus: function (status: number) { return status >= 200 && status < 300; } }); const jsonResponse = response.data; // Check if response has base64-encoded image content if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) { let content = jsonResponse.content; const responseMimeType = jsonResponse.mimeType || mimeType; // The content field contains base64-encoded data, decode it first if (typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) { try { const decodedString = atob(content); // Check if it's JSON (nested structure) or direct image data if (decodedString.startsWith('{')) { // It's JSON, parse it const nestedJson = JSON.parse(decodedString); if (nestedJson && typeof nestedJson === 'object' && 'content' in nestedJson) { const innerContent = nestedJson.content; const isBase64 = /^[A-Za-z0-9+/=]+$/.test(innerContent); if (isBase64) { // It's base64-encoded image content content = innerContent; } else { throw new Error('Inner content is not base64-encoded'); } } } else if (decodedString.startsWith('\x89PNG') || decodedString.startsWith('\xFF\xD8\xFF') || decodedString.startsWith('GIF8') || decodedString.startsWith('RIFF')) { // It's direct image data, use it as is content = btoa(decodedString); // Re-encode as base64 for processing } else { throw new Error('Decoded content is neither JSON nor image data'); } } catch (decodeError) { console.warn('⚠️ Failed to decode base64 content:', decodeError); throw decodeError; } } // Decode base64 content let decodedContent; try { decodedContent = atob(content); // Verify it's actually an image by checking for common image headers const isJPEG = decodedContent.startsWith('\xFF\xD8\xFF'); const isPNG = decodedContent.startsWith('\x89PNG\r\n\x1a\n'); const isGIF = decodedContent.startsWith('GIF8'); const isWebP = decodedContent.startsWith('RIFF') && decodedContent.includes('WEBP'); if (!isJPEG && !isPNG && !isGIF && !isWebP) { console.warn('⚠️ Decoded content does not appear to be a valid image'); } } catch (decodeError) { console.error('❌ Failed to decode base64 image content:', decodeError); throw new Error('Failed to decode image content'); } // Create a blob from the decoded image content // Convert string to Uint8Array for proper binary handling const uint8Array = new Uint8Array(decodedContent.length); for (let i = 0; i < decodedContent.length; i++) { uint8Array[i] = decodedContent.charCodeAt(i); } const blob = new Blob([uint8Array], { type: responseMimeType }); const url = window.URL.createObjectURL(blob); return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent }; } else { throw new Error('No content field in image response'); } } catch (jsonError) { // Fallback to blob response const response = await api.get(`/api/files/${fileId}/preview`, { responseType: 'blob', validateStatus: function (status: number) { return status >= 200 && status < 300; } }); const previewData = response.data; const url = window.URL.createObjectURL(previewData); return { success: true, previewUrl: url, blob: previewData, isJsonContent: false }; } } // For other files, first try to get JSON response (for text-based files) try { const response = await api.get(`/api/files/${fileId}/preview`, { responseType: 'json', validateStatus: function (status: number) { return status >= 200 && status < 300; } }); const jsonResponse = response.data; // Check if response has content field (structured response) if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) { const content = jsonResponse.content; const mimeType = jsonResponse.mimeType || 'text/plain'; // Check if content is base64 encoded (common pattern) let decodedContent = content; try { // Try to decode as base64 if it looks like base64 if (content && typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) { decodedContent = atob(content); } } catch (decodeError) { decodedContent = content; } // Create a blob from the (possibly decoded) content const blob = new Blob([decodedContent], { type: mimeType }); const url = window.URL.createObjectURL(blob); return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent }; } else if (jsonResponse && typeof jsonResponse === 'object' && 'result' in jsonResponse) { // Handle base64 encoded content in 'result' field try { // Decode base64 content const decodedContent = atob(jsonResponse.result); const mimeType = jsonResponse.mimeType || 'application/json'; // Create a blob from the decoded content const blob = new Blob([decodedContent], { type: mimeType }); const url = window.URL.createObjectURL(blob); return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent }; } catch (decodeError) { console.error('❌ Failed to decode base64 content:', decodeError); // Fallback to treating as raw JSON const blob = new Blob([JSON.stringify(jsonResponse, null, 2)], { type: 'application/json' }); const url = window.URL.createObjectURL(blob); return { success: true, previewUrl: url, blob: blob, isJsonContent: true }; } } else { // If it's not structured JSON, treat as raw content const blob = new Blob([JSON.stringify(jsonResponse, null, 2)], { type: 'application/json' }); const url = window.URL.createObjectURL(blob); return { success: true, previewUrl: url, blob: blob, isJsonContent: true }; } } catch (jsonError) { // Fallback to blob response for binary files const response = await api.get(`/api/files/${fileId}/preview`, { responseType: 'blob', validateStatus: function (status: number) { return status >= 200 && status < 300; } }); const previewData = response.data; // Create a blob URL for preview const url = window.URL.createObjectURL(previewData); return { success: true, previewUrl: url, blob: previewData, isJsonContent: false }; } } catch (error: any) { console.error(`❌ Preview failed for ${fileName}:`, error); let errorMessage = error.message; if (error.response?.status === 404) { errorMessage = `File "${fileName}" not found or has been deleted.`; } else if (error.response?.status === 403) { errorMessage = `No permission to preview "${fileName}".`; } else if (error.response?.status === 415) { errorMessage = `File type "${fileName}" is not supported for preview.`; } setPreviewError(errorMessage); return { success: false, error: errorMessage }; } finally { setPreviewingFiles(prev => { const newSet = new Set(prev); newSet.delete(fileId); return newSet; }); } }; // Generic inline update handler for FormGeneratorTable const handleInlineUpdate = async (fileId: string, changes: Partial<{ fileName: string }>, existingRow?: any) => { if (!existingRow) { throw new Error('Existing row data required for inline update'); } const result = await handleFileUpdate(fileId, changes, existingRow); if (!result.success) { throw new Error(result.error || 'Failed to update'); } return result; }; return { downloadingFiles, deletingFiles, editingFiles, uploadingFile, downloadError, deleteError, uploadError, previewingFiles, previewError, handleFileDownload, handleFileDelete, handleFileDeleteMultiple, handleFileUpload, handleFileUpdate, handleFilePreview, handleInlineUpdate, isLoading }; }