ui-nyla/src/components/UnifiedDataBar/FilesTab.tsx
2026-04-19 01:22:34 +02:00

304 lines
10 KiB
TypeScript

import React, { useState, useCallback, useRef, useMemo } from 'react';
import type { UdbContext } from './UnifiedDataBar';
import api from '../../api';
import FolderTree from '../../components/FolderTree/FolderTree';
import type { FileNode } from '../../components/FolderTree/FolderTree';
import { useFileContext } from '../../contexts/FileContext';
import styles from './FilesTab.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
interface FilesTabProps {
context: UdbContext;
onFileSelect?: (fileId: string, fileName?: string) => void;
onSendToChat?: (items: Array<{ id: string; type: 'file' | 'folder'; name: string }>) => void;
}
const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect, onSendToChat }) => {
const { t } = useLanguage();
const [searchQuery, setSearchQuery] = useState('');
const [isDragOver, setIsDragOver] = useState(false);
const [uploading, setUploading] = useState(false);
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const {
folders,
refreshFolders,
treeFileNodes,
treeFilesLoading,
refreshTreeFiles,
updateTreeFileNode,
expandedFolderIds,
toggleFolderExpanded,
handleCreateFolder,
handleRenameFolder,
handleDeleteFolder,
handleMoveFolder,
handleMoveFolders,
handleMoveFile,
handleMoveFiles: contextMoveFiles,
handleFileDelete,
handleDownloadFolder,
} = useFileContext();
const _folderNodes = useMemo(() => {
return folders.map(f => ({
id: f.id,
name: f.name,
parentId: f.parentId ?? null,
fileCount: f.fileCount ?? 0,
neutralize: f.neutralize ?? false,
scope: f.scope ?? 'personal',
}));
}, [folders]);
const _fileNodes: FileNode[] = useMemo(() => {
let result = treeFileNodes;
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase();
result = result.filter(f =>
f.fileName.toLowerCase().includes(q),
);
}
return result;
}, [treeFileNodes, searchQuery]);
const _refreshAll = useCallback(async () => {
await Promise.all([refreshTreeFiles(), refreshFolders()]);
}, [refreshTreeFiles, refreshFolders]);
const _uploadFiles = useCallback(async (fileList: FileList | File[]) => {
if (!context.instanceId || uploading) return;
setUploading(true);
try {
for (const file of Array.from(fileList)) {
const formData = new FormData();
formData.append('file', file);
formData.append('featureInstanceId', context.instanceId);
await api.post('/api/files/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
}
await _refreshAll();
} catch (err) {
console.error('File upload failed:', err);
} finally {
setUploading(false);
}
}, [context.instanceId, uploading, _refreshAll]);
const _handleDragOver = useCallback((e: React.DragEvent) => {
if (e.dataTransfer.types.includes('Files')) {
e.preventDefault();
e.stopPropagation();
setIsDragOver(true);
}
}, []);
const _handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
}, []);
const _handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
if (e.dataTransfer.files.length > 0) {
_uploadFiles(e.dataTransfer.files);
}
}, [_uploadFiles]);
const _handleFileInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
_uploadFiles(e.target.files);
e.target.value = '';
}
}, [_uploadFiles]);
const _onMoveFile = useCallback(async (fileId: string, targetFolderId: string | null) => {
await handleMoveFile(fileId, targetFolderId);
}, [handleMoveFile]);
const _onMoveFiles = useCallback(async (fileIds: string[], targetFolderId: string | null) => {
await contextMoveFiles(fileIds, targetFolderId);
}, [contextMoveFiles]);
const _onDeleteFolder = useCallback(async (folderId: string) => {
await handleDeleteFolder(folderId);
if (selectedFolderId === folderId) setSelectedFolderId(null);
}, [handleDeleteFolder, selectedFolderId]);
const _onRenameFile = useCallback(async (fileId: string, newName: string) => {
await api.put(`/api/files/${fileId}`, { fileName: newName });
await refreshTreeFiles();
}, [refreshTreeFiles]);
const _onDeleteFile = useCallback(async (fileId: string) => {
await handleFileDelete(fileId);
}, [handleFileDelete]);
const _onDeleteFiles = useCallback(async (fileIds: string[]) => {
await api.post('/api/files/batch-delete', { fileIds });
await Promise.all([refreshTreeFiles(), refreshFolders()]);
}, [refreshTreeFiles, refreshFolders]);
const _onDeleteFolders = useCallback(async (folderIds: string[]) => {
await api.post('/api/files/batch-delete', { folderIds, recursiveFolders: true });
await Promise.all([refreshFolders(), refreshTreeFiles()]);
}, [refreshFolders, refreshTreeFiles]);
const _onScopeChange = useCallback(async (fileId: string, newScope: string) => {
updateTreeFileNode(fileId, { scope: newScope });
try {
await api.patch(`/api/files/${fileId}/scope`, { scope: newScope });
} catch (err) {
console.error('Failed to update scope:', err);
await refreshTreeFiles();
}
}, [updateTreeFileNode, refreshTreeFiles]);
const _onNeutralizeToggle = useCallback(async (fileId: string, newValue: boolean) => {
updateTreeFileNode(fileId, { neutralize: newValue });
try {
await api.patch(`/api/files/${fileId}/neutralize`, { neutralize: newValue });
} catch (err) {
console.error('Failed to toggle neutralize:', err);
await refreshTreeFiles();
}
}, [updateTreeFileNode, refreshTreeFiles]);
const _onFolderNeutralizeToggle = useCallback(async (folderId: string, newValue: boolean) => {
try {
await api.patch(`/api/files/folders/${folderId}/neutralize`, { neutralize: newValue });
await refreshFolders();
await refreshTreeFiles();
} catch (err) {
console.error('Failed to toggle folder neutralize:', err);
}
}, [refreshFolders, refreshTreeFiles]);
const _onFolderScopeChange = useCallback(async (folderId: string, newScope: string) => {
try {
await api.patch(`/api/files/folders/${folderId}/scope`, { scope: newScope });
await refreshFolders();
await refreshTreeFiles();
} catch (err) {
console.error('Failed to change folder scope:', err);
}
}, [refreshFolders, refreshTreeFiles]);
if (treeFilesLoading && treeFileNodes.length === 0) {
return <div className={styles.loading}>{t('Dateien laden')}</div>;
}
return (
<div
className={styles.filesTab}
onDragOver={_handleDragOver}
onDragLeave={_handleDragLeave}
onDrop={_handleDrop}
>
{isDragOver && (
<div style={{
position: 'absolute', inset: 0,
background: 'rgba(25, 118, 210, 0.08)',
border: '2px dashed #F25843', borderRadius: 8,
zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 13, fontWeight: 600, color: '#F25843',
}}>
{t('Dateien hier ablegen')}
</div>
)}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 8px' }}>
<span style={{ fontSize: 12, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>{t('Dateien')}</span>
<div style={{ display: 'flex', gap: 4 }}>
<button
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: '#F25843' }}
title={t('Dateien hochladen')}
>
{uploading ? '...' : '+'}
</button>
<button
onClick={_refreshAll}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: '#F25843' }}
>
{'\u21BB'}
</button>
</div>
</div>
<input
ref={fileInputRef}
type="file"
multiple
style={{ display: 'none' }}
onChange={_handleFileInputChange}
/>
<input
type="text"
placeholder={t('Dateien suchen')}
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
style={{
width: '100%', padding: '6px 10px', fontSize: 12, borderRadius: 4,
border: '1px solid #ddd', boxSizing: 'border-box', margin: '0 0 4px',
}}
/>
<div style={{ flex: 1, overflow: 'auto' }}>
<FolderTree
folders={_folderNodes}
files={_fileNodes}
showFiles={true}
selectedFolderId={selectedFolderId}
onSelect={setSelectedFolderId}
onFileSelect={onFileSelect ? (fileId: string) => {
const file = treeFileNodes.find(f => f.id === fileId);
onFileSelect(fileId, file?.fileName);
} : undefined}
expandedIds={expandedFolderIds}
onToggleExpand={toggleFolderExpanded}
onRefresh={_refreshAll}
onCreateFolder={handleCreateFolder}
onRenameFolder={handleRenameFolder}
onDeleteFolder={_onDeleteFolder}
onMoveFolder={handleMoveFolder}
onMoveFolders={handleMoveFolders}
onMoveFile={_onMoveFile}
onMoveFiles={_onMoveFiles}
onRenameFile={_onRenameFile}
onDeleteFile={_onDeleteFile}
onDeleteFiles={_onDeleteFiles}
onDeleteFolders={_onDeleteFolders}
onDownloadFolder={handleDownloadFolder}
onScopeChange={_onScopeChange}
onNeutralizeToggle={_onNeutralizeToggle}
onFolderScopeChange={_onFolderScopeChange}
onFolderNeutralizeToggle={_onFolderNeutralizeToggle}
onSendToChat={onSendToChat}
/>
{_fileNodes.length === 0 && (
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
{searchQuery ? t('Keine Dateien gefunden') : t('Keine Dateien. Drag & Drop zum Hochladen.')}
</div>
)}
</div>
<div className={styles.legend}>
<span>{'\uD83D\uDC64'} {t('Persönlich')}</span>
<span>{'\uD83D\uDC65'} {t('Instanz')}</span>
<span>{'\uD83C\uDFE2'} {t('Mandant')}</span>
<span>{'\uD83D\uDD12'} {t('Neutralisiert')}</span>
</div>
</div>
);
};
export default FilesTab;