frontend_nyla/src/components/UnifiedDataBar/FilesTab.tsx
2026-03-28 21:46:54 +01:00

329 lines
10 KiB
TypeScript

import React, { useState, useCallback, useRef, useMemo, useEffect } 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';
interface FileEntry {
id: string;
fileName: string;
mimeType?: string;
fileSize?: number;
folderId?: string | null;
tags?: string[];
scope: string;
neutralize: boolean;
}
interface FilesTabProps {
context: UdbContext;
onFileSelect?: (fileId: string) => void;
}
const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
const [files, setFiles] = useState<FileEntry[]>([]);
const [loading, setLoading] = useState(true);
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,
handleCreateFolder,
handleRenameFolder,
handleDeleteFolder,
handleMoveFolder,
handleMoveFolders,
handleMoveFile,
handleMoveFiles: contextMoveFiles,
handleFileDelete,
handleDownloadFolder,
expandedFolderIds,
toggleFolderExpanded,
} = useFileContext();
const _loadFiles = useCallback(async () => {
setLoading(true);
try {
const response = await api.get(`/api/workspace/${context.instanceId}/files`);
const body = response.data;
const rawList =
(Array.isArray(body?.files) && body.files) ||
(Array.isArray(body?.data) && body.data) ||
(Array.isArray(body) ? body : []);
setFiles(
rawList.map((f: any) => ({
id: f.id,
fileName: f.fileName || f.name || 'unknown',
mimeType: f.mimeType,
fileSize: f.fileSize,
folderId: f.folderId ?? null,
tags: f.tags || [],
scope: f.scope || 'personal',
neutralize: f.neutralize || false,
})),
);
} catch (err) {
console.error('Failed to load files:', err);
} finally {
setLoading(false);
}
}, [context.instanceId]);
useEffect(() => {
_loadFiles();
}, [_loadFiles]);
const _folderNodes = useMemo(() =>
folders.map(f => ({
id: f.id,
name: f.name,
parentId: f.parentId ?? null,
})),
[folders],
);
const _fileNodes: FileNode[] = useMemo(() => {
let result = files;
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase();
result = result.filter(f =>
f.fileName.toLowerCase().includes(q)
|| (f.tags || []).some((t: string) => t.toLowerCase().includes(q)),
);
}
return result
.sort((a, b) => a.fileName.localeCompare(b.fileName))
.map(f => ({
id: f.id,
fileName: f.fileName,
mimeType: f.mimeType,
fileSize: f.fileSize,
folderId: f.folderId ?? null,
scope: f.scope,
neutralize: f.neutralize,
}));
}, [files, searchQuery]);
const _refreshAll = useCallback(() => {
_loadFiles();
refreshFolders();
}, [_loadFiles, 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' },
});
}
_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);
_loadFiles();
}, [handleMoveFile, _loadFiles]);
const _onMoveFiles = useCallback(async (fileIds: string[], targetFolderId: string | null) => {
await contextMoveFiles(fileIds, targetFolderId);
_loadFiles();
}, [contextMoveFiles, _loadFiles]);
const _onDeleteFolder = useCallback(async (folderId: string) => {
await handleDeleteFolder(folderId);
if (selectedFolderId === folderId) setSelectedFolderId(null);
_loadFiles();
}, [handleDeleteFolder, selectedFolderId, _loadFiles]);
const _onRenameFile = useCallback(async (fileId: string, newName: string) => {
await api.put(`/api/files/${fileId}`, { fileName: newName });
_loadFiles();
}, [_loadFiles]);
const _onDeleteFile = useCallback(async (fileId: string) => {
await handleFileDelete(fileId);
_loadFiles();
}, [handleFileDelete, _loadFiles]);
const _onDeleteFiles = useCallback(async (fileIds: string[]) => {
await api.post('/api/files/batch-delete', { fileIds });
_loadFiles();
}, [_loadFiles]);
const _onDeleteFolders = useCallback(async (folderIds: string[]) => {
await api.post('/api/files/batch-delete', { folderIds, recursiveFolders: true });
refreshFolders();
_loadFiles();
}, [refreshFolders, _loadFiles]);
const _onScopeChange = useCallback(async (fileId: string, newScope: string) => {
setFiles(prev => prev.map(f => (f.id === fileId ? { ...f, scope: newScope } : f)));
try {
await api.patch(`/api/files/${fileId}/scope`, { scope: newScope });
} catch (err) {
console.error('Failed to update scope:', err);
_loadFiles();
}
}, [_loadFiles]);
const _onNeutralizeToggle = useCallback(async (fileId: string, newValue: boolean) => {
setFiles(prev => prev.map(f => (f.id === fileId ? { ...f, neutralize: newValue } : f)));
try {
await api.patch(`/api/files/${fileId}/neutralize`, { neutralize: newValue });
} catch (err) {
console.error('Failed to toggle neutralize:', err);
_loadFiles();
}
}, [_loadFiles]);
if (loading) return <div className={styles.loading}>Lade Dateien...</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 #1976d2', borderRadius: 8,
zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 13, fontWeight: 600, color: '#1976d2',
}}>
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' }}>Files</span>
<div style={{ display: 'flex', gap: 4 }}>
<button
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: '#1976d2' }}
title="Upload files"
>
{uploading ? '...' : '+'}
</button>
<button
onClick={_refreshAll}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: '#1976d2' }}
>
{'\u21BB'}
</button>
</div>
</div>
<input
ref={fileInputRef}
type="file"
multiple
style={{ display: 'none' }}
onChange={_handleFileInputChange}
/>
<input
type="text"
placeholder="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}
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}
/>
{_fileNodes.length === 0 && (
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
{searchQuery ? 'Keine Dateien gefunden' : 'Keine Dateien. Drag & Drop zum Hochladen.'}
</div>
)}
</div>
<div className={styles.legend}>
<span>{'\uD83D\uDC64'} Persönlich</span>
<span>{'\uD83D\uDC65'} Instanz</span>
<span>{'\uD83C\uDFE2'} Mandant</span>
<span>{'\uD83D\uDD12'} Neutralisiert</span>
</div>
</div>
);
};
export default FilesTab;