import React, { useMemo } from 'react'; import { ViewActionButton, DeleteActionButton, RemoveActionButton, EditActionButton, CopyActionButton, CustomActionButton } from '../../FormGenerator/ActionButtons'; import { FaDownload, FaLink, FaPlay } from 'react-icons/fa'; import { WorkflowFile } from '../../../hooks/usePlayground'; import styles from './ConnectedFilesList.module.css'; import { formatBinaryDataSizeBytes } from '../../../utils/formatDataSize'; import { useLanguage } from '../../../providers/language/LanguageContext'; export interface ConnectedFilesListActionButton { type: 'edit' | 'delete' | 'download' | 'view' | 'copy' | 'connect' | 'play' | 'remove'; onAction?: (file: WorkflowFile) => Promise | void; disabled?: (file: WorkflowFile) => boolean | { disabled: boolean; message?: string }; loading?: (file: WorkflowFile) => boolean; title?: string | ((file: WorkflowFile) => string); className?: string; // For download and view buttons isProcessing?: (file: WorkflowFile) => boolean; // Field mappings for flexible data access idField?: string; // Field name for the unique identifier (default: 'fileId') nameField?: string; // Field name for display name (default: 'fileName') typeField?: string; // Field name for type/mime type (default: 'mimeType') contentField?: string; // Field name for content (used by copy button) statusField?: string; // Field name for status (used by connect button) authorityField?: string; // Field name for authority (msft/google) (used by connect button) // Operation and loading state names operationName?: string; // Name of the operation function in hookData refreshOperationName?: string; // Name of the refresh operation function in hookData (for connect button) loadingStateName?: string; // Name of the loading state in hookData // Navigation (for play button) navigateTo?: string; // Route to navigate to when play button is clicked // Special handling for remove button showOnlyForPending?: boolean; // Show remove button only for pending files (default: true for 'remove' type) } export interface ConnectedFilesListProps { files: WorkflowFile[]; pendingFiles?: WorkflowFile[]; // New: Configurable action buttons (takes precedence over legacy props) actionButtons?: ConnectedFilesListActionButton[]; // Legacy props (kept for backward compatibility, used as defaults if actionButtons not provided) onDelete?: (file: WorkflowFile) => Promise; onRemove?: (file: WorkflowFile) => Promise; onAttach?: (fileId: string) => Promise; // Attach file for next message deletingFiles?: Set; previewingFiles?: Set; removingFiles?: Set; workflowId?: string; emptyMessage?: string; } export function ConnectedFilesList({ files, pendingFiles = [], actionButtons, onDelete, onRemove, onAttach, deletingFiles = new Set(), previewingFiles = new Set(), removingFiles = new Set(), workflowId: _workflowId, emptyMessage = 'No files connected to this workflow' }: ConnectedFilesListProps) { const { t } = useLanguage(); // Combine workflow files and pending files, deduplicating by fileId const allFiles = useMemo(() => { const fileMap = new Map(); // Add workflow files first (filter out files without fileId) files.forEach(file => { if (file.fileId && file.fileId.trim() !== '') { fileMap.set(file.fileId, file); } }); // Add pending files (may override workflow files if same fileId) pendingFiles.forEach(file => { if (file.fileId && file.fileId.trim() !== '') { fileMap.set(file.fileId, file); } }); return Array.from(fileMap.values()); }, [files, pendingFiles]); // Create hookData object for action buttons const hookData = useMemo(() => ({ handleDelete: async (fileId: string) => { const file = allFiles.find(f => f.fileId === fileId); if (file && onDelete) { await onDelete(file); return true; } return false; }, removeOptimistically: (_fileId: string) => { // This will be handled by the parent component's state }, refetch: async () => { // Refetch handled by parent }, deletingItems: deletingFiles, previewingFiles: previewingFiles, removingItems: removingFiles }), [allFiles, onDelete, deletingFiles, previewingFiles, removingFiles]); // Generate default action buttons from legacy props if actionButtons not provided const defaultActionButtons = useMemo(() => { if (actionButtons) { return actionButtons; } // Legacy behavior: create default buttons from old props const buttons: ConnectedFilesListActionButton[] = []; // View button (always shown) buttons.push({ type: 'view', onAction: async (_file: WorkflowFile) => { // View is handled by ViewActionButton's FilePreview component return Promise.resolve(); }, idField: 'fileId', nameField: 'fileName', typeField: 'mimeType' }); // Remove button (only for pending files, if onRemove provided) if (onRemove) { buttons.push({ type: 'remove', onAction: async (file: WorkflowFile) => { await onRemove(file); }, showOnlyForPending: true, idField: 'fileId', loadingStateName: 'removingItems' }); } // Delete button (always shown, if onDelete provided) if (onDelete) { buttons.push({ type: 'delete', operationName: 'handleDelete', loadingStateName: 'deletingItems', idField: 'fileId' }); } return buttons; }, [actionButtons, onDelete, onRemove]); const handleView = async (_file: WorkflowFile) => { // View is handled by ViewActionButton's FilePreview component return Promise.resolve(); }; const handleRemove = async (file: WorkflowFile) => { // Remove file from workflow (not delete from backend) if (onRemove) { await onRemove(file); } }; if (allFiles.length === 0) { return (
{emptyMessage}
); } return (

{t('connectedFilesList.connectedFiles')}

({allFiles.length})
{allFiles .filter(file => file.fileId && file.fileId.trim() !== '') // Ensure fileId exists .map((file) => { // const isDeleting = deletingFiles.has(file.fileId!); // const isPreviewing = previewingFiles.has(file.fileId!); // const isRemoving = removingFiles.has(file.fileId!); // Use fileId as key since we've filtered out files without it const uniqueKey = file.fileId!; // Check if file is in pending files (can be removed) or in messages (already sent) const isPendingFile = pendingFiles.some(f => f.fileId === file.fileId); // Handle clicking on file item to attach/detach for next message const handleFileItemClick = async (e: React.MouseEvent) => { // Don't trigger if clicking on action buttons - they handle their own clicks const target = e.target as HTMLElement; const fileActionsElement = target.closest(`.${styles.fileActions}`); const buttonElement = target.closest('button'); if (fileActionsElement || buttonElement) { e.stopPropagation(); return; } // Prevent default and stop propagation to ensure click handler fires e.preventDefault(); e.stopPropagation(); if (onAttach && file.fileId) { console.log('🖱️ ConnectedFilesList: Clicking file to attach/detach:', file.fileId); await onAttach(file.fileId); } }; return (
{file.fileName} {onAttach && ( {isPendingFile ? t('connectedFilesList.clickToDetach') : t('connectedFilesList.clickToAttach')} )}
{formatBinaryDataSizeBytes(file.fileSize)} {file.source && ( {file.source === 'user_uploaded' ? t('connectedFilesList.uploaded') : t('connectedFilesList.aiCreated')} )} {isPendingFile && ( • Attached )}
e.stopPropagation()}> {defaultActionButtons.map((actionButton, actionIndex) => { // Check if button should be shown for this file if (actionButton.showOnlyForPending !== undefined) { const shouldShow = actionButton.showOnlyForPending ? isPendingFile : !isPendingFile; if (!shouldShow) { return null; } } const actionTitle = typeof actionButton.title === 'function' ? actionButton.title(file) : actionButton.title; const disabledResult = actionButton.disabled ? actionButton.disabled(file) : false; const isLoading = actionButton.loading ? actionButton.loading(file) : false; const isProcessing = actionButton.isProcessing ? actionButton.isProcessing(file) : false; const baseProps = { row: file, disabled: disabledResult, loading: isLoading, className: actionButton.className, title: actionTitle, idField: actionButton.idField ?? 'fileId', nameField: actionButton.nameField ?? 'fileName', typeField: actionButton.typeField ?? 'mimeType', contentField: actionButton.contentField ?? 'content', statusField: actionButton.statusField ?? 'status', authorityField: actionButton.authorityField ?? 'authority', operationName: actionButton.operationName, refreshOperationName: actionButton.refreshOperationName, loadingStateName: actionButton.loadingStateName, hookData: hookData }; switch (actionButton.type) { case 'edit': return ; case 'delete': return ; case 'download': return } onClick={actionButton.onAction || (() => {})} disabled={() => disabledResult} loading={() => isProcessing} title={actionTitle} className={actionButton.className} />; case 'view': return ; case 'copy': return ; case 'connect': return } onClick={actionButton.onAction || (() => {})} disabled={() => disabledResult} loading={() => isLoading} title={actionTitle} className={actionButton.className} />; case 'play': return } onClick={actionButton.onAction || (() => {})} disabled={() => disabledResult} loading={() => isLoading} title={actionTitle} className={actionButton.className} />; case 'remove': return ; default: return null; } })}
); })}
); } export default ConnectedFilesList;