fixed udb foldertree
This commit is contained in:
parent
994664f0b4
commit
dfb4c5ebd7
10 changed files with 438 additions and 378 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
// api.ts
|
// api.ts
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { addCSRFTokenToHeaders, getCSRFToken, generateAndStoreCSRFToken } from './utils/csrfUtils';
|
import { addCSRFTokenToHeaders, getCSRFToken, generateAndStoreCSRFToken } from './utils/csrfUtils';
|
||||||
import { clearUserDataCache } from './utils/userCache';
|
import { clearUserDataCache, getUserDataCache } from './utils/userCache';
|
||||||
|
|
||||||
// Utility function to resolve hostname to IP address
|
// Utility function to resolve hostname to IP address
|
||||||
const resolveHostnameToIP = async (hostname: string): Promise<string | null> => {
|
const resolveHostnameToIP = async (hostname: string): Promise<string | null> => {
|
||||||
|
|
@ -85,6 +85,13 @@ api.interceptors.request.use(
|
||||||
console.log('🍪 Using httpOnly cookies for authentication (automatic)');
|
console.log('🍪 Using httpOnly cookies for authentication (automatic)');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send app language to backend so i18n labels match the UI
|
||||||
|
const userData = getUserDataCache();
|
||||||
|
const appLanguage = userData?.language || navigator.language.split('-')[0] || 'de';
|
||||||
|
if (config.headers) {
|
||||||
|
config.headers['Accept-Language'] = appLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
// Add multi-tenant context headers from URL (if not already set)
|
// Add multi-tenant context headers from URL (if not already set)
|
||||||
// This ensures Feature-Instance roles are loaded for permission checks
|
// This ensures Feature-Instance roles are loaded for permission checks
|
||||||
const context = getContextFromUrl();
|
const context = getContextFromUrl();
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,7 @@ export interface FolderInfo {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
parentId: string | null;
|
parentId: string | null;
|
||||||
|
fileCount?: number;
|
||||||
mandateId?: string;
|
mandateId?: string;
|
||||||
featureInstanceId?: string;
|
featureInstanceId?: string;
|
||||||
createdAt?: number;
|
createdAt?: number;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export interface FolderNode {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
parentId: string | null;
|
parentId: string | null;
|
||||||
|
fileCount?: number;
|
||||||
children?: FolderNode[];
|
children?: FolderNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -363,7 +364,7 @@ function _TreeNode({
|
||||||
const isNavSelected = selectedFolderId === node.id;
|
const isNavSelected = selectedFolderId === node.id;
|
||||||
const isMultiSelected = sel.selectedItemIds.has(node.id);
|
const isMultiSelected = sel.selectedItemIds.has(node.id);
|
||||||
const folderFiles = showFiles ? (filesByFolder.get(node.id) || []) : [];
|
const folderFiles = showFiles ? (filesByFolder.get(node.id) || []) : [];
|
||||||
const hasChildren = (node.children && node.children.length > 0) || folderFiles.length > 0;
|
const hasChildren = (node.children && node.children.length > 0) || folderFiles.length > 0 || (node.fileCount ?? 0) > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (renaming && inputRef.current) inputRef.current.focus();
|
if (renaming && inputRef.current) inputRef.current.focus();
|
||||||
|
|
@ -609,7 +610,7 @@ export default function FolderTree({
|
||||||
if (next.has(id)) next.delete(id); else next.add(id);
|
if (next.has(id)) next.delete(id); else next.add(id);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, []);
|
}, [onToggleExpand]);
|
||||||
|
|
||||||
const _setSelection = useCallback((ids: Set<string>) => {
|
const _setSelection = useCallback((ids: Set<string>) => {
|
||||||
if (onSelectionChange) {
|
if (onSelectionChange) {
|
||||||
|
|
|
||||||
|
|
@ -141,9 +141,7 @@
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 11px;
|
font-size: 13px;
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
color: var(--color-text-secondary, #64748b);
|
color: var(--color-text-secondary, #64748b);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|
|
||||||
|
|
@ -223,11 +223,13 @@ function FilterValuesList({
|
||||||
allValues,
|
allValues,
|
||||||
activeFilter,
|
activeFilter,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
resolveLabel,
|
||||||
}: {
|
}: {
|
||||||
columnKey: string;
|
columnKey: string;
|
||||||
allValues: string[];
|
allValues: string[];
|
||||||
activeFilter: any;
|
activeFilter: any;
|
||||||
onSelect: (value: string) => void;
|
onSelect: (value: string) => void;
|
||||||
|
resolveLabel?: (value: string) => string;
|
||||||
}) {
|
}) {
|
||||||
const [displayCount, setDisplayCount] = useState(_FILTER_PAGE_SIZE);
|
const [displayCount, setDisplayCount] = useState(_FILTER_PAGE_SIZE);
|
||||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -256,16 +258,19 @@ function FilterValuesList({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{visibleValues.map(value => (
|
{visibleValues.map(value => {
|
||||||
<div
|
const label = resolveLabel ? resolveLabel(value) : value;
|
||||||
key={value}
|
return (
|
||||||
className={`${styles.filterOption} ${activeFilter === value ? styles.filterOptionSelected : ''}`}
|
<div
|
||||||
onClick={() => onSelect(value)}
|
key={value}
|
||||||
title={value}
|
className={`${styles.filterOption} ${activeFilter === value ? styles.filterOptionSelected : ''}`}
|
||||||
>
|
onClick={() => onSelect(value)}
|
||||||
{value.length > 30 ? value.substring(0, 30) + '...' : value}
|
title={label}
|
||||||
</div>
|
>
|
||||||
))}
|
{label.length > 30 ? label.substring(0, 30) + '...' : label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
{displayCount < allValues.length && (
|
{displayCount < allValues.length && (
|
||||||
<div ref={sentinelRef} style={{ height: 1, opacity: 0 }} />
|
<div ref={sentinelRef} style={{ height: 1, opacity: 0 }} />
|
||||||
)}
|
)}
|
||||||
|
|
@ -884,6 +889,9 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
// Skip if column has static filterOptions (enum) – those are used directly
|
// Skip if column has static filterOptions (enum) – those are used directly
|
||||||
if (column?.filterOptions && column.filterOptions.length > 0) return;
|
if (column?.filterOptions && column.filterOptions.length > 0) return;
|
||||||
|
|
||||||
|
// FK columns: extract values from actual data instead of backend endpoint
|
||||||
|
if (column?.fkSource) return;
|
||||||
|
|
||||||
// Skip if already loaded or currently loading
|
// Skip if already loaded or currently loading
|
||||||
if (asyncFilterValues[openFilterColumn] || filterValuesLoading[openFilterColumn]) return;
|
if (asyncFilterValues[openFilterColumn] || filterValuesLoading[openFilterColumn]) return;
|
||||||
|
|
||||||
|
|
@ -932,6 +940,22 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
return column.filterOptions;
|
return column.filterOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FK columns: extract distinct values from actual data (Excel autofilter style)
|
||||||
|
if (column?.fkSource) {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
data.forEach(row => {
|
||||||
|
const val = row[columnKey];
|
||||||
|
if (val && typeof val === 'string' && val.trim()) {
|
||||||
|
seen.add(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Array.from(seen).sort((a, b) => {
|
||||||
|
const labelA = fkCache[column.fkSource!]?.[a] || a;
|
||||||
|
const labelB = fkCache[column.fkSource!]?.[b] || b;
|
||||||
|
return labelA.localeCompare(labelB);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (asyncFilterValues[columnKey] && asyncFilterValues[columnKey].length > 0) {
|
if (asyncFilterValues[columnKey] && asyncFilterValues[columnKey].length > 0) {
|
||||||
return asyncFilterValues[columnKey];
|
return asyncFilterValues[columnKey];
|
||||||
}
|
}
|
||||||
|
|
@ -945,7 +969,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [detectedColumns, asyncFilterValues, apiEndpoint, hookData]);
|
}, [detectedColumns, asyncFilterValues, apiEndpoint, hookData, data, fkCache]);
|
||||||
|
|
||||||
// Close filter dropdown when clicking outside
|
// Close filter dropdown when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -2012,6 +2036,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
allValues={getUniqueValuesForColumn(column.key)}
|
allValues={getUniqueValuesForColumn(column.key)}
|
||||||
activeFilter={filters[column.key]}
|
activeFilter={filters[column.key]}
|
||||||
onSelect={(value) => handleFilter(column.key, value)}
|
onSelect={(value) => handleFilter(column.key, value)}
|
||||||
|
resolveLabel={column.fkSource ? (val) => fkCache[column.fkSource!]?.[val] || val : undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,12 @@
|
||||||
import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
|
import React, { useState, useCallback, useRef, useMemo } from 'react';
|
||||||
import type { UdbContext } from './UnifiedDataBar';
|
import type { UdbContext } from './UnifiedDataBar';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
import FolderTree from '../../components/FolderTree/FolderTree';
|
import FolderTree from '../../components/FolderTree/FolderTree';
|
||||||
import type { FileNode } from '../../components/FolderTree/FolderTree';
|
import type { FileNode } from '../../components/FolderTree/FolderTree';
|
||||||
import { useFileContext } from '../../contexts/FileContext';
|
import { useFileContext } from '../../contexts/FileContext';
|
||||||
import styles from './FilesTab.module.css';
|
import styles from './FilesTab.module.css';
|
||||||
|
|
||||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||||
|
|
||||||
interface FileEntry {
|
|
||||||
id: string;
|
|
||||||
fileName: string;
|
|
||||||
mimeType?: string;
|
|
||||||
fileSize?: number;
|
|
||||||
folderId?: string | null;
|
|
||||||
tags?: string[];
|
|
||||||
scope: string;
|
|
||||||
neutralize: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilesTabProps {
|
interface FilesTabProps {
|
||||||
context: UdbContext;
|
context: UdbContext;
|
||||||
onFileSelect?: (fileId: string, fileName?: string) => void;
|
onFileSelect?: (fileId: string, fileName?: string) => void;
|
||||||
|
|
@ -26,8 +14,6 @@ interface FilesTabProps {
|
||||||
|
|
||||||
const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const [files, setFiles] = useState<FileEntry[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
@ -37,6 +23,11 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
||||||
const {
|
const {
|
||||||
folders,
|
folders,
|
||||||
refreshFolders,
|
refreshFolders,
|
||||||
|
treeFileNodes,
|
||||||
|
treeFilesLoading,
|
||||||
|
refreshTreeFiles,
|
||||||
|
expandedFolderIds,
|
||||||
|
toggleFolderExpanded,
|
||||||
handleCreateFolder,
|
handleCreateFolder,
|
||||||
handleRenameFolder,
|
handleRenameFolder,
|
||||||
handleDeleteFolder,
|
handleDeleteFolder,
|
||||||
|
|
@ -46,85 +37,32 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
||||||
handleMoveFiles: contextMoveFiles,
|
handleMoveFiles: contextMoveFiles,
|
||||||
handleFileDelete,
|
handleFileDelete,
|
||||||
handleDownloadFolder,
|
handleDownloadFolder,
|
||||||
expandedFolderIds,
|
|
||||||
toggleFolderExpanded,
|
|
||||||
} = useFileContext();
|
} = 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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const _onFileUploaded = () => {
|
|
||||||
setTimeout(() => _loadFiles(), 150);
|
|
||||||
};
|
|
||||||
window.addEventListener('fileUploaded', _onFileUploaded as EventListener);
|
|
||||||
return () => window.removeEventListener('fileUploaded', _onFileUploaded as EventListener);
|
|
||||||
}, [_loadFiles]);
|
|
||||||
|
|
||||||
const _folderNodes = useMemo(() =>
|
const _folderNodes = useMemo(() =>
|
||||||
folders.map(f => ({
|
folders.map(f => ({
|
||||||
id: f.id,
|
id: f.id,
|
||||||
name: f.name,
|
name: f.name,
|
||||||
parentId: f.parentId ?? null,
|
parentId: f.parentId ?? null,
|
||||||
|
fileCount: f.fileCount ?? 0,
|
||||||
})),
|
})),
|
||||||
[folders],
|
[folders],
|
||||||
);
|
);
|
||||||
|
|
||||||
const _fileNodes: FileNode[] = useMemo(() => {
|
const _fileNodes: FileNode[] = useMemo(() => {
|
||||||
let result = files;
|
let result = treeFileNodes;
|
||||||
if (searchQuery.trim()) {
|
if (searchQuery.trim()) {
|
||||||
const q = searchQuery.toLowerCase();
|
const q = searchQuery.toLowerCase();
|
||||||
result = result.filter(f =>
|
result = result.filter(f =>
|
||||||
f.fileName.toLowerCase().includes(q)
|
f.fileName.toLowerCase().includes(q),
|
||||||
|| (f.tags || []).some((t: string) => t.toLowerCase().includes(q)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return result
|
return result;
|
||||||
.sort((a, b) => a.fileName.localeCompare(b.fileName))
|
}, [treeFileNodes, searchQuery]);
|
||||||
.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(() => {
|
const _refreshAll = useCallback(async () => {
|
||||||
_loadFiles();
|
await Promise.all([refreshTreeFiles(), refreshFolders()]);
|
||||||
refreshFolders();
|
}, [refreshTreeFiles, refreshFolders]);
|
||||||
}, [_loadFiles, refreshFolders]);
|
|
||||||
|
|
||||||
const _uploadFiles = useCallback(async (fileList: FileList | File[]) => {
|
const _uploadFiles = useCallback(async (fileList: FileList | File[]) => {
|
||||||
if (!context.instanceId || uploading) return;
|
if (!context.instanceId || uploading) return;
|
||||||
|
|
@ -138,7 +76,7 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_refreshAll();
|
await _refreshAll();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('File upload failed:', err);
|
console.error('File upload failed:', err);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -178,62 +116,57 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
||||||
|
|
||||||
const _onMoveFile = useCallback(async (fileId: string, targetFolderId: string | null) => {
|
const _onMoveFile = useCallback(async (fileId: string, targetFolderId: string | null) => {
|
||||||
await handleMoveFile(fileId, targetFolderId);
|
await handleMoveFile(fileId, targetFolderId);
|
||||||
_loadFiles();
|
}, [handleMoveFile]);
|
||||||
}, [handleMoveFile, _loadFiles]);
|
|
||||||
|
|
||||||
const _onMoveFiles = useCallback(async (fileIds: string[], targetFolderId: string | null) => {
|
const _onMoveFiles = useCallback(async (fileIds: string[], targetFolderId: string | null) => {
|
||||||
await contextMoveFiles(fileIds, targetFolderId);
|
await contextMoveFiles(fileIds, targetFolderId);
|
||||||
_loadFiles();
|
}, [contextMoveFiles]);
|
||||||
}, [contextMoveFiles, _loadFiles]);
|
|
||||||
|
|
||||||
const _onDeleteFolder = useCallback(async (folderId: string) => {
|
const _onDeleteFolder = useCallback(async (folderId: string) => {
|
||||||
await handleDeleteFolder(folderId);
|
await handleDeleteFolder(folderId);
|
||||||
if (selectedFolderId === folderId) setSelectedFolderId(null);
|
if (selectedFolderId === folderId) setSelectedFolderId(null);
|
||||||
_loadFiles();
|
}, [handleDeleteFolder, selectedFolderId]);
|
||||||
}, [handleDeleteFolder, selectedFolderId, _loadFiles]);
|
|
||||||
|
|
||||||
const _onRenameFile = useCallback(async (fileId: string, newName: string) => {
|
const _onRenameFile = useCallback(async (fileId: string, newName: string) => {
|
||||||
await api.put(`/api/files/${fileId}`, { fileName: newName });
|
await api.put(`/api/files/${fileId}`, { fileName: newName });
|
||||||
_loadFiles();
|
await refreshTreeFiles();
|
||||||
}, [_loadFiles]);
|
}, [refreshTreeFiles]);
|
||||||
|
|
||||||
const _onDeleteFile = useCallback(async (fileId: string) => {
|
const _onDeleteFile = useCallback(async (fileId: string) => {
|
||||||
await handleFileDelete(fileId);
|
await handleFileDelete(fileId);
|
||||||
_loadFiles();
|
}, [handleFileDelete]);
|
||||||
}, [handleFileDelete, _loadFiles]);
|
|
||||||
|
|
||||||
const _onDeleteFiles = useCallback(async (fileIds: string[]) => {
|
const _onDeleteFiles = useCallback(async (fileIds: string[]) => {
|
||||||
await api.post('/api/files/batch-delete', { fileIds });
|
await api.post('/api/files/batch-delete', { fileIds });
|
||||||
_loadFiles();
|
await Promise.all([refreshTreeFiles(), refreshFolders()]);
|
||||||
}, [_loadFiles]);
|
}, [refreshTreeFiles, refreshFolders]);
|
||||||
|
|
||||||
const _onDeleteFolders = useCallback(async (folderIds: string[]) => {
|
const _onDeleteFolders = useCallback(async (folderIds: string[]) => {
|
||||||
await api.post('/api/files/batch-delete', { folderIds, recursiveFolders: true });
|
await api.post('/api/files/batch-delete', { folderIds, recursiveFolders: true });
|
||||||
refreshFolders();
|
await Promise.all([refreshFolders(), refreshTreeFiles()]);
|
||||||
_loadFiles();
|
}, [refreshFolders, refreshTreeFiles]);
|
||||||
}, [refreshFolders, _loadFiles]);
|
|
||||||
|
|
||||||
const _onScopeChange = useCallback(async (fileId: string, newScope: string) => {
|
const _onScopeChange = useCallback(async (fileId: string, newScope: string) => {
|
||||||
setFiles(prev => prev.map(f => (f.id === fileId ? { ...f, scope: newScope } : f)));
|
|
||||||
try {
|
try {
|
||||||
await api.patch(`/api/files/${fileId}/scope`, { scope: newScope });
|
await api.patch(`/api/files/${fileId}/scope`, { scope: newScope });
|
||||||
|
await refreshTreeFiles();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to update scope:', err);
|
console.error('Failed to update scope:', err);
|
||||||
_loadFiles();
|
|
||||||
}
|
}
|
||||||
}, [_loadFiles]);
|
}, [refreshTreeFiles]);
|
||||||
|
|
||||||
const _onNeutralizeToggle = useCallback(async (fileId: string, newValue: boolean) => {
|
const _onNeutralizeToggle = useCallback(async (fileId: string, newValue: boolean) => {
|
||||||
setFiles(prev => prev.map(f => (f.id === fileId ? { ...f, neutralize: newValue } : f)));
|
|
||||||
try {
|
try {
|
||||||
await api.patch(`/api/files/${fileId}/neutralize`, { neutralize: newValue });
|
await api.patch(`/api/files/${fileId}/neutralize`, { neutralize: newValue });
|
||||||
|
await refreshTreeFiles();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to toggle neutralize:', err);
|
console.error('Failed to toggle neutralize:', err);
|
||||||
_loadFiles();
|
|
||||||
}
|
}
|
||||||
}, [_loadFiles]);
|
}, [refreshTreeFiles]);
|
||||||
|
|
||||||
if (loading) return <div className={styles.loading}>{t('Dateien laden')}</div>;
|
if (treeFilesLoading && treeFileNodes.length === 0) {
|
||||||
|
return <div className={styles.loading}>{t('Dateien laden')}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -301,7 +234,7 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
||||||
selectedFolderId={selectedFolderId}
|
selectedFolderId={selectedFolderId}
|
||||||
onSelect={setSelectedFolderId}
|
onSelect={setSelectedFolderId}
|
||||||
onFileSelect={onFileSelect ? (fileId: string) => {
|
onFileSelect={onFileSelect ? (fileId: string) => {
|
||||||
const file = files.find(f => f.id === fileId);
|
const file = treeFileNodes.find(f => f.id === fileId);
|
||||||
onFileSelect(fileId, file?.fileName);
|
onFileSelect(fileId, file?.fileName);
|
||||||
} : undefined}
|
} : undefined}
|
||||||
expandedIds={expandedFolderIds}
|
expandedIds={expandedFolderIds}
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,60 @@
|
||||||
import React, { createContext, useContext, useCallback, useState, useEffect } from 'react';
|
import React, { createContext, useContext, useCallback, useState, useEffect, useMemo } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { useUserFiles, useFileOperations, UserFile } from '../hooks/useFiles';
|
import { useFileOperations } from '../hooks/useFiles';
|
||||||
import type { FolderInfo } from '../api/fileApi';
|
import type { FolderInfo } from '../api/fileApi';
|
||||||
|
import type { FileNode } from '../components/FolderTree/FolderTree';
|
||||||
|
|
||||||
export type { FolderInfo };
|
export type { FolderInfo };
|
||||||
|
|
||||||
interface FileContextType {
|
interface FileContextType {
|
||||||
files: UserFile[];
|
folders: FolderInfo[];
|
||||||
loading: boolean;
|
foldersLoading: boolean;
|
||||||
error: string | null;
|
refreshFolders: () => Promise<void>;
|
||||||
refetch: () => Promise<void>;
|
|
||||||
handleFileUpload: (file: File, workflowId?: string) => Promise<{ success: boolean; fileData?: any; error?: string }>;
|
treeFileNodes: FileNode[];
|
||||||
|
treeFilesLoading: boolean;
|
||||||
|
loadTreeFiles: (folderId: string) => Promise<void>;
|
||||||
|
refreshTreeFiles: () => Promise<void>;
|
||||||
|
|
||||||
|
expandedFolderIds: Set<string>;
|
||||||
|
toggleFolderExpanded: (id: string) => void;
|
||||||
|
|
||||||
|
handleCreateFolder: (name: string, parentId: string | null) => Promise<void>;
|
||||||
|
handleRenameFolder: (folderId: string, newName: string) => Promise<void>;
|
||||||
|
handleDeleteFolder: (folderId: string) => Promise<void>;
|
||||||
|
handleMoveFolder: (folderId: string, targetParentId: string | null) => Promise<void>;
|
||||||
|
handleMoveFolders: (folderIds: string[], targetParentId: string | null) => Promise<void>;
|
||||||
|
handleMoveFile: (fileId: string, targetFolderId: string | null) => Promise<void>;
|
||||||
|
handleMoveFiles: (fileIds: string[], targetFolderId: string | null) => Promise<void>;
|
||||||
|
handleDownloadFolder: (folderId: string, folderName: string) => Promise<void>;
|
||||||
handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise<boolean>;
|
handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise<boolean>;
|
||||||
|
handleFileUpload: (file: File, workflowId?: string) => Promise<{ success: boolean; fileData?: any; error?: string }>;
|
||||||
handleFilePreview: (fileId: string, fileName: string, mimeType?: string) => Promise<{ success: boolean; previewUrl?: string | null; blob?: Blob | null; isJsonContent?: boolean; decodedContent?: any; isTextContent?: boolean; error?: string }>;
|
handleFilePreview: (fileId: string, fileName: string, mimeType?: string) => Promise<{ success: boolean; previewUrl?: string | null; blob?: Blob | null; isJsonContent?: boolean; decodedContent?: any; isTextContent?: boolean; error?: string }>;
|
||||||
handleFileDownload: (fileId: string, fileName: string) => Promise<void>;
|
handleFileDownload: (fileId: string, fileName: string) => Promise<void>;
|
||||||
uploadingFile: boolean;
|
uploadingFile: boolean;
|
||||||
deletingFiles: Set<string>;
|
deletingFiles: Set<string>;
|
||||||
previewingFiles: Set<string>;
|
previewingFiles: Set<string>;
|
||||||
downloadingFiles: Set<string>;
|
downloadingFiles: Set<string>;
|
||||||
folders: FolderInfo[];
|
|
||||||
foldersLoading: boolean;
|
|
||||||
refreshFolders: () => Promise<void>;
|
|
||||||
handleCreateFolder: (name: string, parentId: string | null) => Promise<void>;
|
|
||||||
handleRenameFolder: (folderId: string, newName: string) => Promise<void>;
|
|
||||||
handleDeleteFolder: (folderId: string) => Promise<void>;
|
|
||||||
handleMoveFolder: (folderId: string, targetParentId: string | null) => Promise<void>;
|
|
||||||
handleMoveFile: (fileId: string, targetFolderId: string | null) => Promise<void>;
|
|
||||||
handleMoveFiles: (fileIds: string[], targetFolderId: string | null) => Promise<void>;
|
|
||||||
handleMoveFolders: (folderIds: string[], targetParentId: string | null) => Promise<void>;
|
|
||||||
handleDownloadFolder: (folderId: string, folderName: string) => Promise<void>;
|
|
||||||
expandedFolderIds: Set<string>;
|
|
||||||
toggleFolderExpanded: (id: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileContext = createContext<FileContextType | undefined>(undefined);
|
export const FileContext = createContext<FileContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
const _ROOT_KEY = '__root__';
|
||||||
|
|
||||||
|
function _toFileNode(f: any): FileNode {
|
||||||
|
return {
|
||||||
|
id: f.id,
|
||||||
|
fileName: f.fileName || f.name || 'unknown',
|
||||||
|
mimeType: f.mimeType,
|
||||||
|
fileSize: f.fileSize,
|
||||||
|
folderId: f.folderId ?? null,
|
||||||
|
scope: f.scope,
|
||||||
|
neutralize: f.neutralize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function FileProvider({ children }: { children: React.ReactNode }) {
|
export function FileProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { data: files, loading, error, refetch: refetchFiles, removeFileOptimistically } = useUserFiles();
|
|
||||||
const {
|
const {
|
||||||
handleFileUpload: hookHandleFileUpload,
|
handleFileUpload: hookHandleFileUpload,
|
||||||
handleFileDelete: hookHandleFileDelete,
|
handleFileDelete: hookHandleFileDelete,
|
||||||
|
|
@ -45,30 +63,33 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
|
||||||
uploadingFile,
|
uploadingFile,
|
||||||
deletingFiles,
|
deletingFiles,
|
||||||
previewingFiles,
|
previewingFiles,
|
||||||
downloadingFiles
|
downloadingFiles,
|
||||||
} = useFileOperations();
|
} = useFileOperations();
|
||||||
|
|
||||||
useEffect(() => { refetchFiles(); }, []);
|
// ── Derive a session-scoped storage key from the current feature-instance URL ──
|
||||||
|
const location = useLocation();
|
||||||
|
const storageKey = useMemo(() => {
|
||||||
|
const match = location.pathname.match(/^\/mandates\/([^/]+)\/([^/]+)\/([^/]+)/);
|
||||||
|
const instanceId = match ? match[3] : '_global';
|
||||||
|
return `folderTree-expandedIds-${instanceId}`;
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
// ── Folder expanded state (persisted in localStorage) ───────────────────
|
// ── Folder expanded state (persisted per feature-instance in sessionStorage) ──
|
||||||
const _STORAGE_KEY = 'folderTree-expandedIds';
|
|
||||||
const [expandedFolderIds, setExpandedFolderIds] = useState<Set<string>>(() => {
|
const [expandedFolderIds, setExpandedFolderIds] = useState<Set<string>>(() => {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(_STORAGE_KEY);
|
const stored = sessionStorage.getItem(storageKey);
|
||||||
return stored ? new Set<string>(JSON.parse(stored)) : new Set<string>();
|
return stored ? new Set<string>(JSON.parse(stored)) : new Set<string>();
|
||||||
} catch { return new Set<string>(); }
|
} catch { return new Set<string>(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleFolderExpanded = useCallback((id: string) => {
|
useEffect(() => {
|
||||||
setExpandedFolderIds(prev => {
|
try {
|
||||||
const next = new Set(prev);
|
const stored = sessionStorage.getItem(storageKey);
|
||||||
if (next.has(id)) next.delete(id); else next.add(id);
|
setExpandedFolderIds(stored ? new Set<string>(JSON.parse(stored)) : new Set<string>());
|
||||||
try { localStorage.setItem(_STORAGE_KEY, JSON.stringify([...next])); } catch {}
|
} catch { setExpandedFolderIds(new Set<string>()); }
|
||||||
return next;
|
}, [storageKey]);
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ── Folder state (single source of truth) ──────────────────────────────
|
// ── Folder state ──────────────────────────────────────────────────────
|
||||||
const [folders, setFolders] = useState<FolderInfo[]>([]);
|
const [folders, setFolders] = useState<FolderInfo[]>([]);
|
||||||
const [foldersLoading, setFoldersLoading] = useState(false);
|
const [foldersLoading, setFoldersLoading] = useState(false);
|
||||||
|
|
||||||
|
|
@ -87,6 +108,92 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
|
||||||
useEffect(() => { refreshFolders(); }, [refreshFolders]);
|
useEffect(() => { refreshFolders(); }, [refreshFolders]);
|
||||||
|
|
||||||
|
// ── Tree files: lazy-loaded per expanded folder ───────────────────────
|
||||||
|
const [treeFilesMap, setTreeFilesMap] = useState<Map<string, FileNode[]>>(new Map());
|
||||||
|
const [treeFilesLoading, setTreeFilesLoading] = useState(false);
|
||||||
|
|
||||||
|
const loadTreeFiles = useCallback(async (folderId: string) => {
|
||||||
|
const key = folderId || _ROOT_KEY;
|
||||||
|
setTreeFilesLoading(true);
|
||||||
|
try {
|
||||||
|
const filterValue = folderId || null;
|
||||||
|
const resp = await api.get('/api/files/list', {
|
||||||
|
params: {
|
||||||
|
pagination: JSON.stringify({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 500,
|
||||||
|
filters: { folderId: filterValue },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const items: any[] = resp.data?.items || [];
|
||||||
|
setTreeFilesMap(prev => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.set(key, items.map(_toFileNode));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to load tree files for folder ${folderId}:`, err);
|
||||||
|
} finally {
|
||||||
|
setTreeFilesLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const _removeTreeFiles = useCallback((folderId: string) => {
|
||||||
|
const key = folderId || _ROOT_KEY;
|
||||||
|
setTreeFilesMap(prev => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.delete(key);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const refreshTreeFiles = useCallback(async () => {
|
||||||
|
const keys = Array.from(treeFilesMap.keys());
|
||||||
|
await Promise.all(
|
||||||
|
keys.map(key => loadTreeFiles(key === _ROOT_KEY ? '' : key)),
|
||||||
|
);
|
||||||
|
}, [treeFilesMap, loadTreeFiles]);
|
||||||
|
|
||||||
|
// Load root files on mount
|
||||||
|
useEffect(() => { loadTreeFiles(''); }, [loadTreeFiles]);
|
||||||
|
|
||||||
|
// Load files for initially expanded folders
|
||||||
|
useEffect(() => {
|
||||||
|
expandedFolderIds.forEach(id => {
|
||||||
|
if (!treeFilesMap.has(id)) {
|
||||||
|
loadTreeFiles(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Only on mount – don't re-run when treeFilesMap changes
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const treeFileNodes: FileNode[] = useMemo(() => {
|
||||||
|
const result: FileNode[] = [];
|
||||||
|
for (const [, files] of treeFilesMap) {
|
||||||
|
for (const f of files) result.push(f);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [treeFilesMap]);
|
||||||
|
|
||||||
|
// ── Toggle expand: load/unload tree files ─────────────────────────────
|
||||||
|
const toggleFolderExpanded = useCallback((id: string) => {
|
||||||
|
setExpandedFolderIds(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) {
|
||||||
|
next.delete(id);
|
||||||
|
_removeTreeFiles(id);
|
||||||
|
} else {
|
||||||
|
next.add(id);
|
||||||
|
loadTreeFiles(id);
|
||||||
|
}
|
||||||
|
try { sessionStorage.setItem(storageKey, JSON.stringify([...next])); } catch {}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [storageKey, loadTreeFiles, _removeTreeFiles]);
|
||||||
|
|
||||||
|
// ── Folder operations ─────────────────────────────────────────────────
|
||||||
const handleCreateFolder = useCallback(async (name: string, parentId: string | null) => {
|
const handleCreateFolder = useCallback(async (name: string, parentId: string | null) => {
|
||||||
await api.post('/api/files/folders', { name, parentId: parentId || null });
|
await api.post('/api/files/folders', { name, parentId: parentId || null });
|
||||||
await refreshFolders();
|
await refreshFolders();
|
||||||
|
|
@ -99,30 +206,53 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
|
||||||
const handleDeleteFolder = useCallback(async (folderId: string) => {
|
const handleDeleteFolder = useCallback(async (folderId: string) => {
|
||||||
await api.delete(`/api/files/folders/${folderId}`, { params: { recursive: true } });
|
await api.delete(`/api/files/folders/${folderId}`, { params: { recursive: true } });
|
||||||
|
_removeTreeFiles(folderId);
|
||||||
await refreshFolders();
|
await refreshFolders();
|
||||||
await refetchFiles();
|
}, [refreshFolders, _removeTreeFiles]);
|
||||||
}, [refreshFolders, refetchFiles]);
|
|
||||||
|
|
||||||
const handleMoveFolder = useCallback(async (folderId: string, targetParentId: string | null) => {
|
const handleMoveFolder = useCallback(async (folderId: string, targetParentId: string | null) => {
|
||||||
await api.post(`/api/files/folders/${folderId}/move`, { targetParentId });
|
await api.post(`/api/files/folders/${folderId}/move`, { targetParentId });
|
||||||
await refreshFolders();
|
await refreshFolders();
|
||||||
}, [refreshFolders]);
|
}, [refreshFolders]);
|
||||||
|
|
||||||
const handleMoveFile = useCallback(async (fileId: string, targetFolderId: string | null) => {
|
|
||||||
await api.post(`/api/files/${fileId}/move`, { targetFolderId });
|
|
||||||
await refetchFiles();
|
|
||||||
}, [refetchFiles]);
|
|
||||||
|
|
||||||
const handleMoveFiles = useCallback(async (fileIds: string[], targetFolderId: string | null) => {
|
|
||||||
await api.post('/api/files/batch-move', { fileIds, targetFolderId });
|
|
||||||
await refetchFiles();
|
|
||||||
}, [refetchFiles]);
|
|
||||||
|
|
||||||
const handleMoveFolders = useCallback(async (folderIds: string[], targetParentId: string | null) => {
|
const handleMoveFolders = useCallback(async (folderIds: string[], targetParentId: string | null) => {
|
||||||
await api.post('/api/files/batch-move', { folderIds, targetParentId });
|
await api.post('/api/files/batch-move', { folderIds, targetParentId });
|
||||||
await refreshFolders();
|
await refreshFolders();
|
||||||
}, [refreshFolders]);
|
}, [refreshFolders]);
|
||||||
|
|
||||||
|
// ── File operations ───────────────────────────────────────────────────
|
||||||
|
const handleMoveFile = useCallback(async (fileId: string, targetFolderId: string | null) => {
|
||||||
|
await api.post(`/api/files/${fileId}/move`, { targetFolderId });
|
||||||
|
await refreshTreeFiles();
|
||||||
|
await refreshFolders();
|
||||||
|
}, [refreshTreeFiles, refreshFolders]);
|
||||||
|
|
||||||
|
const handleMoveFiles = useCallback(async (fileIds: string[], targetFolderId: string | null) => {
|
||||||
|
await api.post('/api/files/batch-move', { fileIds, targetFolderId });
|
||||||
|
await refreshTreeFiles();
|
||||||
|
await refreshFolders();
|
||||||
|
}, [refreshTreeFiles, refreshFolders]);
|
||||||
|
|
||||||
|
const handleFileUpload = useCallback(async (file: File, workflowId?: string) => {
|
||||||
|
const result = await hookHandleFileUpload(file, workflowId);
|
||||||
|
if (result.success) {
|
||||||
|
await refreshTreeFiles();
|
||||||
|
await refreshFolders();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [hookHandleFileUpload, refreshTreeFiles, refreshFolders]);
|
||||||
|
|
||||||
|
const handleFileDelete = useCallback(async (fileId: string, onOptimisticDelete?: () => void) => {
|
||||||
|
const success = await hookHandleFileDelete(fileId, () => {
|
||||||
|
onOptimisticDelete?.();
|
||||||
|
});
|
||||||
|
if (success) {
|
||||||
|
await refreshTreeFiles();
|
||||||
|
await refreshFolders();
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}, [hookHandleFileDelete, refreshTreeFiles, refreshFolders]);
|
||||||
|
|
||||||
const handleDownloadFolder = useCallback(async (folderId: string, folderName: string) => {
|
const handleDownloadFolder = useCallback(async (folderId: string, folderName: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/api/files/folders/${folderId}/download`, {
|
const response = await api.get(`/api/files/folders/${folderId}/download`, {
|
||||||
|
|
@ -141,40 +271,28 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ── File operations ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const handleFileUpload = useCallback(async (file: File, workflowId?: string) => {
|
|
||||||
const result = await hookHandleFileUpload(file, workflowId);
|
|
||||||
if (result.success && result.fileData) {
|
|
||||||
await refetchFiles();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, [hookHandleFileUpload, refetchFiles]);
|
|
||||||
|
|
||||||
const handleFileDelete = useCallback(async (fileId: string, onOptimisticDelete?: () => void) => {
|
|
||||||
const success = await hookHandleFileDelete(fileId, () => {
|
|
||||||
removeFileOptimistically(fileId);
|
|
||||||
onOptimisticDelete?.();
|
|
||||||
});
|
|
||||||
if (success) {
|
|
||||||
await refetchFiles();
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}, [hookHandleFileDelete, removeFileOptimistically, refetchFiles]);
|
|
||||||
|
|
||||||
const refetch = useCallback(async () => {
|
|
||||||
await refetchFiles();
|
|
||||||
}, [refetchFiles]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileContext.Provider
|
<FileContext.Provider
|
||||||
value={{
|
value={{
|
||||||
files,
|
folders,
|
||||||
loading,
|
foldersLoading,
|
||||||
error,
|
refreshFolders,
|
||||||
refetch,
|
treeFileNodes,
|
||||||
handleFileUpload,
|
treeFilesLoading,
|
||||||
|
loadTreeFiles,
|
||||||
|
refreshTreeFiles,
|
||||||
|
expandedFolderIds,
|
||||||
|
toggleFolderExpanded,
|
||||||
|
handleCreateFolder,
|
||||||
|
handleRenameFolder,
|
||||||
|
handleDeleteFolder,
|
||||||
|
handleMoveFolder,
|
||||||
|
handleMoveFolders,
|
||||||
|
handleMoveFile,
|
||||||
|
handleMoveFiles,
|
||||||
|
handleDownloadFolder,
|
||||||
handleFileDelete,
|
handleFileDelete,
|
||||||
|
handleFileUpload,
|
||||||
handleFilePreview: handleFilePreview as FileContextType['handleFilePreview'],
|
handleFilePreview: handleFilePreview as FileContextType['handleFilePreview'],
|
||||||
handleFileDownload: async (fileId: string, fileName: string) => {
|
handleFileDownload: async (fileId: string, fileName: string) => {
|
||||||
await handleFileDownload(fileId, fileName);
|
await handleFileDownload(fileId, fileName);
|
||||||
|
|
@ -183,19 +301,6 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
|
||||||
deletingFiles,
|
deletingFiles,
|
||||||
previewingFiles,
|
previewingFiles,
|
||||||
downloadingFiles,
|
downloadingFiles,
|
||||||
folders,
|
|
||||||
foldersLoading,
|
|
||||||
refreshFolders,
|
|
||||||
handleCreateFolder,
|
|
||||||
handleRenameFolder,
|
|
||||||
handleDeleteFolder,
|
|
||||||
handleMoveFolder,
|
|
||||||
handleMoveFile,
|
|
||||||
handleMoveFiles,
|
|
||||||
handleMoveFolders,
|
|
||||||
handleDownloadFolder,
|
|
||||||
expandedFolderIds,
|
|
||||||
toggleFolderExpanded,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
* FilesPage
|
* FilesPage
|
||||||
*
|
*
|
||||||
* Split-view file management: FolderTree on the left, FormGeneratorTable on the right.
|
* Split-view file management: FolderTree on the left, FormGeneratorTable on the right.
|
||||||
* Uses useResizablePanels for the divider.
|
* The tree is the master – it dictates which folder's files the table shows (paginated).
|
||||||
|
* Tree files are managed by FileContext (lazy-loaded per expanded folder).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
||||||
|
|
@ -12,13 +13,11 @@ import { useFileContext } from '../../contexts/FileContext';
|
||||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||||
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
||||||
import FolderTree from '../../components/FolderTree/FolderTree';
|
import FolderTree from '../../components/FolderTree/FolderTree';
|
||||||
import type { FileNode } from '../../components/FolderTree/FolderTree';
|
|
||||||
import { useResizablePanels } from '../../hooks/useResizablePanels';
|
import { useResizablePanels } from '../../hooks/useResizablePanels';
|
||||||
import { FaSync, FaUpload, FaDownload, FaEye, FaFolderPlus } from 'react-icons/fa';
|
import { FaSync, FaUpload, FaDownload, FaEye, FaFolderPlus } from 'react-icons/fa';
|
||||||
import { useToast } from '../../contexts/ToastContext';
|
import { useToast } from '../../contexts/ToastContext';
|
||||||
import { usePrompt } from '../../hooks/usePrompt';
|
import { usePrompt } from '../../hooks/usePrompt';
|
||||||
import styles from '../admin/Admin.module.css';
|
import styles from '../admin/Admin.module.css';
|
||||||
|
|
||||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||||
|
|
||||||
interface UserFile {
|
interface UserFile {
|
||||||
|
|
@ -32,8 +31,7 @@ interface UserFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FilesPage: React.FC = () => {
|
export const FilesPage: React.FC = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const { showSuccess, showError } = useToast();
|
const { showSuccess, showError } = useToast();
|
||||||
const { prompt: promptInput, PromptDialog } = usePrompt();
|
const { prompt: promptInput, PromptDialog } = usePrompt();
|
||||||
|
|
@ -48,13 +46,14 @@ export const FilesPage: React.FC = () => {
|
||||||
maxLeftWidth: 40,
|
maxLeftWidth: 40,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Table data (paginated, filtered by selectedFolderId) ──────────────
|
||||||
const {
|
const {
|
||||||
data: files,
|
data: tableFiles,
|
||||||
attributes,
|
attributes,
|
||||||
permissions,
|
permissions,
|
||||||
loading,
|
loading: tableLoading,
|
||||||
error,
|
error,
|
||||||
refetch,
|
refetch: tableRefetch,
|
||||||
pagination,
|
pagination,
|
||||||
fetchFileById,
|
fetchFileById,
|
||||||
updateFileOptimistically,
|
updateFileOptimistically,
|
||||||
|
|
@ -74,19 +73,22 @@ export const FilesPage: React.FC = () => {
|
||||||
previewingFiles,
|
previewingFiles,
|
||||||
} = useFileOperations();
|
} = useFileOperations();
|
||||||
|
|
||||||
|
// ── Tree data (from FileContext – lazy-loaded per expanded folder) ─────
|
||||||
const {
|
const {
|
||||||
folders,
|
folders,
|
||||||
refreshFolders,
|
refreshFolders,
|
||||||
|
treeFileNodes,
|
||||||
|
refreshTreeFiles,
|
||||||
|
expandedFolderIds,
|
||||||
|
toggleFolderExpanded,
|
||||||
handleCreateFolder,
|
handleCreateFolder,
|
||||||
handleRenameFolder,
|
handleRenameFolder,
|
||||||
handleDeleteFolder,
|
handleDeleteFolder,
|
||||||
handleMoveFolder,
|
handleMoveFolder,
|
||||||
handleMoveFolders,
|
handleMoveFolders,
|
||||||
handleMoveFile,
|
handleMoveFile: contextMoveFile,
|
||||||
handleMoveFiles: contextMoveFiles,
|
handleMoveFiles: contextMoveFiles,
|
||||||
handleDownloadFolder,
|
handleDownloadFolder,
|
||||||
expandedFolderIds,
|
|
||||||
toggleFolderExpanded,
|
|
||||||
} = useFileContext();
|
} = useFileContext();
|
||||||
|
|
||||||
const [editingFile, setEditingFile] = useState<UserFile | null>(null);
|
const [editingFile, setEditingFile] = useState<UserFile | null>(null);
|
||||||
|
|
@ -94,43 +96,37 @@ export const FilesPage: React.FC = () => {
|
||||||
const [treeSelectedIds, setTreeSelectedIds] = useState<Set<string>>(new Set());
|
const [treeSelectedIds, setTreeSelectedIds] = useState<Set<string>>(new Set());
|
||||||
const [highlightedFileId, setHighlightedFileId] = useState<string | null>(null);
|
const [highlightedFileId, setHighlightedFileId] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => { refetch(); }, []);
|
// ── Table refetch: always includes folderId filter ────────────────────
|
||||||
|
const _tableRefetch = useCallback(async (params?: any) => {
|
||||||
|
const nextParams = { ...(params || {}) };
|
||||||
|
const nextFilters = { ...(nextParams.filters || {}) };
|
||||||
|
nextFilters.folderId = selectedFolderId;
|
||||||
|
nextParams.filters = nextFilters;
|
||||||
|
await tableRefetch(nextParams);
|
||||||
|
}, [tableRefetch, selectedFolderId]);
|
||||||
|
|
||||||
const treeFileNodes: FileNode[] = useMemo(() => {
|
useEffect(() => {
|
||||||
if (!files) return [];
|
_tableRefetch({ page: 1, pageSize: 25 });
|
||||||
return files.map((f: UserFile) => ({
|
}, [selectedFolderId, _tableRefetch]);
|
||||||
|
|
||||||
|
const _refreshAll = useCallback(async () => {
|
||||||
|
await Promise.all([_tableRefetch(), refreshTreeFiles(), refreshFolders()]);
|
||||||
|
}, [_tableRefetch, refreshTreeFiles, refreshFolders]);
|
||||||
|
|
||||||
|
// ── Folder nodes for tree (with fileCount) ────────────────────────────
|
||||||
|
const folderNodes = useMemo(() =>
|
||||||
|
folders.map(f => ({
|
||||||
id: f.id,
|
id: f.id,
|
||||||
fileName: f.fileName,
|
name: f.name,
|
||||||
mimeType: f.mimeType,
|
parentId: f.parentId ?? null,
|
||||||
fileSize: f.fileSize,
|
fileCount: f.fileCount ?? 0,
|
||||||
folderId: f.folderId ?? null,
|
})),
|
||||||
}));
|
[folders],
|
||||||
}, [files]);
|
);
|
||||||
|
|
||||||
const _handleTreeFileSelect = useCallback((fileId: string) => {
|
|
||||||
const file = files?.find((f: UserFile) => f.id === fileId);
|
|
||||||
if (file) {
|
|
||||||
setSelectedFolderId(file.folderId ?? null);
|
|
||||||
setHighlightedFileId(fileId);
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const row = document.querySelector('tr[data-highlighted="true"]');
|
|
||||||
if (row) row.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
});
|
|
||||||
setTimeout(() => setHighlightedFileId(null), 2500);
|
|
||||||
}
|
|
||||||
}, [files]);
|
|
||||||
|
|
||||||
const filteredFiles = useMemo(() => {
|
|
||||||
if (!files) return [];
|
|
||||||
if (selectedFolderId === null) {
|
|
||||||
return files.filter((f: UserFile) => !f.folderId);
|
|
||||||
}
|
|
||||||
return files.filter((f: UserFile) => f.folderId === selectedFolderId);
|
|
||||||
}, [files, selectedFolderId]);
|
|
||||||
|
|
||||||
|
// ── Columns ───────────────────────────────────────────────────────────
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
const hiddenColumns = ['id', 'mandateId', 'fileHash', 'folderId'];
|
const hiddenColumns = ['id', 'fileHash', 'folderId'];
|
||||||
|
|
||||||
const cols = (attributes || [])
|
const cols = (attributes || [])
|
||||||
.filter(attr => !hiddenColumns.includes(attr.name))
|
.filter(attr => !hiddenColumns.includes(attr.name))
|
||||||
.map(attr => ({
|
.map(attr => ({
|
||||||
|
|
@ -146,7 +142,6 @@ export const FilesPage: React.FC = () => {
|
||||||
fkSource: (attr as any).fkSource,
|
fkSource: (attr as any).fkSource,
|
||||||
fkDisplayField: (attr as any).fkDisplayField,
|
fkDisplayField: (attr as any).fkDisplayField,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
cols.push({
|
cols.push({
|
||||||
key: 'sysCreatedBy',
|
key: 'sysCreatedBy',
|
||||||
label: t('Erstellt von'),
|
label: t('Erstellt von'),
|
||||||
|
|
@ -157,8 +152,9 @@ export const FilesPage: React.FC = () => {
|
||||||
width: 150,
|
width: 150,
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
maxWidth: 250,
|
maxWidth: 250,
|
||||||
|
fkSource: '/api/users/',
|
||||||
|
fkDisplayField: 'username',
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
return cols;
|
return cols;
|
||||||
}, [attributes, t]);
|
}, [attributes, t]);
|
||||||
|
|
||||||
|
|
@ -166,6 +162,51 @@ export const FilesPage: React.FC = () => {
|
||||||
const canUpdate = permissions?.update !== 'n';
|
const canUpdate = permissions?.update !== 'n';
|
||||||
const canDelete = permissions?.delete !== 'n';
|
const canDelete = permissions?.delete !== 'n';
|
||||||
|
|
||||||
|
// ── Tree event handlers ───────────────────────────────────────────────
|
||||||
|
const _handleTreeFileSelect = useCallback((fileId: string) => {
|
||||||
|
const file = treeFileNodes.find(f => f.id === fileId);
|
||||||
|
if (file) {
|
||||||
|
setSelectedFolderId(file.folderId ?? null);
|
||||||
|
setHighlightedFileId(fileId);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const row = document.querySelector('tr[data-highlighted="true"]');
|
||||||
|
if (row) row.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
});
|
||||||
|
setTimeout(() => setHighlightedFileId(null), 2500);
|
||||||
|
}
|
||||||
|
}, [treeFileNodes]);
|
||||||
|
|
||||||
|
const _handleMoveFile = useCallback(async (fileId: string, targetFolderId: string | null) => {
|
||||||
|
await contextMoveFile(fileId, targetFolderId);
|
||||||
|
await _tableRefetch();
|
||||||
|
}, [contextMoveFile, _tableRefetch]);
|
||||||
|
|
||||||
|
const _handleMoveFiles = useCallback(async (fileIds: string[], targetFolderId: string | null) => {
|
||||||
|
await contextMoveFiles(fileIds, targetFolderId);
|
||||||
|
await _tableRefetch();
|
||||||
|
}, [contextMoveFiles, _tableRefetch]);
|
||||||
|
|
||||||
|
const _handleRenameFile = useCallback(async (fileId: string, newName: string) => {
|
||||||
|
await handleFileUpdate(fileId, { fileName: newName });
|
||||||
|
await Promise.all([_tableRefetch(), refreshTreeFiles()]);
|
||||||
|
}, [handleFileUpdate, _tableRefetch, refreshTreeFiles]);
|
||||||
|
|
||||||
|
const _handleDeleteTreeFile = useCallback(async (fileId: string) => {
|
||||||
|
await handleFileDelete(fileId);
|
||||||
|
await Promise.all([_tableRefetch(), refreshTreeFiles(), refreshFolders()]);
|
||||||
|
}, [handleFileDelete, _tableRefetch, refreshTreeFiles, refreshFolders]);
|
||||||
|
|
||||||
|
const _handleDeleteTreeFiles = useCallback(async (fileIds: string[]) => {
|
||||||
|
await handleFileDeleteMultiple(fileIds);
|
||||||
|
await Promise.all([_tableRefetch(), refreshTreeFiles(), refreshFolders()]);
|
||||||
|
}, [handleFileDeleteMultiple, _tableRefetch, refreshTreeFiles, refreshFolders]);
|
||||||
|
|
||||||
|
const _handleDeleteTreeFolders = useCallback(async (folderIds: string[]) => {
|
||||||
|
await api.post('/api/files/batch-delete', { folderIds, recursiveFolders: true });
|
||||||
|
await Promise.all([refreshFolders(), refreshTreeFiles(), _tableRefetch()]);
|
||||||
|
}, [refreshFolders, refreshTreeFiles, _tableRefetch]);
|
||||||
|
|
||||||
|
// ── Table event handlers ──────────────────────────────────────────────
|
||||||
const handleEditClick = async (file: UserFile) => {
|
const handleEditClick = async (file: UserFile) => {
|
||||||
const fullFile = await fetchFileById(file.id);
|
const fullFile = await fetchFileById(file.id);
|
||||||
if (fullFile) setEditingFile(fullFile as UserFile);
|
if (fullFile) setEditingFile(fullFile as UserFile);
|
||||||
|
|
@ -178,19 +219,19 @@ export const FilesPage: React.FC = () => {
|
||||||
}, editingFile);
|
}, editingFile);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setEditingFile(null);
|
setEditingFile(null);
|
||||||
refetch();
|
await Promise.all([_tableRefetch(), refreshTreeFiles()]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (file: UserFile) => {
|
const handleDelete = async (file: UserFile) => {
|
||||||
const success = await handleFileDelete(file.id);
|
const success = await handleFileDelete(file.id);
|
||||||
if (success) refetch();
|
if (success) await Promise.all([_tableRefetch(), refreshTreeFiles(), refreshFolders()]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteMultiple = async (filesToDelete: UserFile[]) => {
|
const handleDeleteMultiple = async (filesToDelete: UserFile[]) => {
|
||||||
const ids = filesToDelete.map(f => f.id);
|
const ids = filesToDelete.map(f => f.id);
|
||||||
const success = await handleFileDeleteMultiple(ids);
|
const success = await handleFileDeleteMultiple(ids);
|
||||||
if (success) refetch();
|
if (success) await Promise.all([_tableRefetch(), refreshTreeFiles(), refreshFolders()]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = async (file: UserFile) => {
|
const handleDownload = async (file: UserFile) => {
|
||||||
|
|
@ -207,16 +248,16 @@ export const FilesPage: React.FC = () => {
|
||||||
const handleUploadClick = () => { fileInputRef.current?.click(); };
|
const handleUploadClick = () => { fileInputRef.current?.click(); };
|
||||||
|
|
||||||
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const selectedFiles = e.target.files;
|
const picked = e.target.files;
|
||||||
if (selectedFiles && selectedFiles.length > 0) {
|
if (picked && picked.length > 0) {
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
for (const file of Array.from(selectedFiles)) {
|
for (const file of Array.from(picked)) {
|
||||||
const result = await handleFileUpload(file);
|
const result = await handleFileUpload(file);
|
||||||
if (result?.success) successCount++; else errorCount++;
|
if (result?.success) successCount++; else errorCount++;
|
||||||
}
|
}
|
||||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||||
await refetch();
|
await Promise.all([_tableRefetch(), refreshTreeFiles(), refreshFolders()]);
|
||||||
if (successCount > 0) {
|
if (successCount > 0) {
|
||||||
showSuccess(
|
showSuccess(
|
||||||
t('Upload erfolgreich'),
|
t('Upload erfolgreich'),
|
||||||
|
|
@ -240,62 +281,13 @@ export const FilesPage: React.FC = () => {
|
||||||
const _onRowDragStart = useCallback((e: React.DragEvent<HTMLTableRowElement>, row: UserFile) => {
|
const _onRowDragStart = useCallback((e: React.DragEvent<HTMLTableRowElement>, row: UserFile) => {
|
||||||
const isInSelection = selectedFiles.some(f => f.id === row.id);
|
const isInSelection = selectedFiles.some(f => f.id === row.id);
|
||||||
if (isInSelection && selectedFiles.length > 1) {
|
if (isInSelection && selectedFiles.length > 1) {
|
||||||
const ids = selectedFiles.map(f => f.id);
|
e.dataTransfer.setData('application/file-ids', JSON.stringify(selectedFiles.map(f => f.id)));
|
||||||
e.dataTransfer.setData('application/file-ids', JSON.stringify(ids));
|
|
||||||
} else {
|
} else {
|
||||||
e.dataTransfer.setData('application/file-id', row.id);
|
e.dataTransfer.setData('application/file-id', row.id);
|
||||||
}
|
}
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
}, [selectedFiles]);
|
}, [selectedFiles]);
|
||||||
|
|
||||||
const _handleMoveFilePage = useCallback(async (fileId: string, targetFolderId: string | null) => {
|
|
||||||
await handleMoveFile(fileId, targetFolderId);
|
|
||||||
await refetch();
|
|
||||||
}, [handleMoveFile, refetch]);
|
|
||||||
|
|
||||||
const _handleMoveFiles = useCallback(async (fileIds: string[], targetFolderId: string | null) => {
|
|
||||||
await contextMoveFiles(fileIds, targetFolderId);
|
|
||||||
await refetch();
|
|
||||||
}, [contextMoveFiles, refetch]);
|
|
||||||
|
|
||||||
const _handleRenameFile = useCallback(async (fileId: string, newName: string) => {
|
|
||||||
await handleFileUpdate(fileId, { fileName: newName });
|
|
||||||
await refetch();
|
|
||||||
}, [handleFileUpdate, refetch]);
|
|
||||||
|
|
||||||
const _handleDeleteTreeFile = useCallback(async (fileId: string) => {
|
|
||||||
await handleFileDelete(fileId);
|
|
||||||
await refetch();
|
|
||||||
}, [handleFileDelete, refetch]);
|
|
||||||
|
|
||||||
const _handleDeleteTreeFiles = useCallback(async (fileIds: string[]) => {
|
|
||||||
await handleFileDeleteMultiple(fileIds);
|
|
||||||
await refetch();
|
|
||||||
}, [handleFileDeleteMultiple, refetch]);
|
|
||||||
|
|
||||||
const _handleDeleteTreeFolders = useCallback(async (folderIds: string[]) => {
|
|
||||||
await api.post('/api/files/batch-delete', { folderIds, recursiveFolders: true });
|
|
||||||
await refreshFolders();
|
|
||||||
await refetch();
|
|
||||||
}, [refreshFolders, refetch]);
|
|
||||||
|
|
||||||
const _handleTreeRefresh = useCallback(async () => {
|
|
||||||
await refetch();
|
|
||||||
await refreshFolders();
|
|
||||||
}, [refetch, refreshFolders]);
|
|
||||||
|
|
||||||
const _tableRefetch = useCallback(async (params?: any) => {
|
|
||||||
const nextParams = { ...(params || {}) };
|
|
||||||
const nextFilters = { ...(nextParams.filters || {}) };
|
|
||||||
nextFilters.folderId = selectedFolderId;
|
|
||||||
nextParams.filters = nextFilters;
|
|
||||||
await refetch(nextParams);
|
|
||||||
}, [refetch, selectedFolderId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
_tableRefetch({ page: 1, pageSize: 25 });
|
|
||||||
}, [selectedFolderId, _tableRefetch]);
|
|
||||||
|
|
||||||
const formAttributes = useMemo(() => {
|
const formAttributes = useMemo(() => {
|
||||||
const excludedFields = ['id', 'mandateId', 'fileHash', 'sysCreatedBy', 'sysCreatedAt', 'sysModifiedAt', 'creationDate', 'source'];
|
const excludedFields = ['id', 'mandateId', 'fileHash', 'sysCreatedBy', 'sysCreatedAt', 'sysModifiedAt', 'creationDate', 'source'];
|
||||||
return (attributes || []).filter(attr => !excludedFields.includes(attr.name));
|
return (attributes || []).filter(attr => !excludedFields.includes(attr.name));
|
||||||
|
|
@ -307,7 +299,7 @@ export const FilesPage: React.FC = () => {
|
||||||
<div className={styles.errorContainer}>
|
<div className={styles.errorContainer}>
|
||||||
<span className={styles.errorIcon}>⚠️</span>
|
<span className={styles.errorIcon}>⚠️</span>
|
||||||
<p className={styles.errorMessage}>{t('Fehler beim Laden der Dateien: {detail}', { detail: String(error) })}</p>
|
<p className={styles.errorMessage}>{t('Fehler beim Laden der Dateien: {detail}', { detail: String(error) })}</p>
|
||||||
<button className={styles.secondaryButton} onClick={() => refetch()}>
|
<button className={styles.secondaryButton} onClick={() => _tableRefetch()}>
|
||||||
<FaSync /> {t('Erneut versuchen')}
|
<FaSync /> {t('Erneut versuchen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -331,13 +323,12 @@ export const FilesPage: React.FC = () => {
|
||||||
<p className={styles.pageSubtitle}>{t('Dateiverwaltung')}</p>
|
<p className={styles.pageSubtitle}>{t('Dateiverwaltung')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.headerActions}>
|
<div className={styles.headerActions}>
|
||||||
<button className={styles.secondaryButton} onClick={() => { refetch(); refreshFolders(); }} disabled={loading}>
|
<button className={styles.secondaryButton} onClick={() => _refreshAll()} disabled={tableLoading}>
|
||||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
<FaSync className={tableLoading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Split-view container */}
|
|
||||||
<div
|
<div
|
||||||
ref={containerRef as React.RefObject<HTMLDivElement>}
|
ref={containerRef as React.RefObject<HTMLDivElement>}
|
||||||
style={{ display: 'flex', flex: 1, overflow: 'hidden', minHeight: 0, position: 'relative' }}
|
style={{ display: 'flex', flex: 1, overflow: 'hidden', minHeight: 0, position: 'relative' }}
|
||||||
|
|
@ -351,7 +342,7 @@ export const FilesPage: React.FC = () => {
|
||||||
padding: '8px 4px',
|
padding: '8px 4px',
|
||||||
}}>
|
}}>
|
||||||
<FolderTree
|
<FolderTree
|
||||||
folders={folders}
|
folders={folderNodes}
|
||||||
files={treeFileNodes}
|
files={treeFileNodes}
|
||||||
showFiles={true}
|
showFiles={true}
|
||||||
selectedFolderId={selectedFolderId}
|
selectedFolderId={selectedFolderId}
|
||||||
|
|
@ -361,17 +352,17 @@ export const FilesPage: React.FC = () => {
|
||||||
onSelectionChange={setTreeSelectedIds}
|
onSelectionChange={setTreeSelectedIds}
|
||||||
expandedIds={expandedFolderIds}
|
expandedIds={expandedFolderIds}
|
||||||
onToggleExpand={toggleFolderExpanded}
|
onToggleExpand={toggleFolderExpanded}
|
||||||
onRefresh={_handleTreeRefresh}
|
onRefresh={_refreshAll}
|
||||||
onCreateFolder={handleCreateFolder}
|
onCreateFolder={handleCreateFolder}
|
||||||
onRenameFolder={handleRenameFolder}
|
onRenameFolder={handleRenameFolder}
|
||||||
onDeleteFolder={async (folderId) => {
|
onDeleteFolder={async (folderId) => {
|
||||||
await handleDeleteFolder(folderId);
|
await handleDeleteFolder(folderId);
|
||||||
if (selectedFolderId === folderId) setSelectedFolderId(null);
|
if (selectedFolderId === folderId) setSelectedFolderId(null);
|
||||||
await refetch();
|
await _tableRefetch();
|
||||||
}}
|
}}
|
||||||
onMoveFolder={handleMoveFolder}
|
onMoveFolder={handleMoveFolder}
|
||||||
onMoveFolders={handleMoveFolders}
|
onMoveFolders={handleMoveFolders}
|
||||||
onMoveFile={_handleMoveFilePage}
|
onMoveFile={_handleMoveFile}
|
||||||
onMoveFiles={_handleMoveFiles}
|
onMoveFiles={_handleMoveFiles}
|
||||||
onRenameFile={_handleRenameFile}
|
onRenameFile={_handleRenameFile}
|
||||||
onDeleteFile={_handleDeleteTreeFile}
|
onDeleteFile={_handleDeleteTreeFile}
|
||||||
|
|
@ -398,7 +389,6 @@ export const FilesPage: React.FC = () => {
|
||||||
|
|
||||||
{/* Right panel: File table */}
|
{/* Right panel: File table */}
|
||||||
<div style={{ flex: 1, minWidth: 0, overflow: 'auto', display: 'flex', flexDirection: 'column' }}>
|
<div style={{ flex: 1, minWidth: 0, overflow: 'auto', display: 'flex', flexDirection: 'column' }}>
|
||||||
{/* Toolbar above table */}
|
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', gap: 8, padding: '8px 12px',
|
display: 'flex', gap: 8, padding: '8px 12px',
|
||||||
borderBottom: '1px solid var(--color-border, #e0e0e0)',
|
borderBottom: '1px solid var(--color-border, #e0e0e0)',
|
||||||
|
|
@ -414,70 +404,68 @@ export const FilesPage: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table content */}
|
|
||||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
<div style={{ flex: 1, overflow: 'auto' }}>
|
||||||
<FormGeneratorTable
|
<FormGeneratorTable
|
||||||
data={filteredFiles}
|
data={tableFiles || []}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
apiEndpoint="/api/files/list"
|
apiEndpoint="/api/files/list"
|
||||||
loading={loading}
|
loading={tableLoading}
|
||||||
pagination={true}
|
pagination={true}
|
||||||
pageSize={25}
|
pageSize={25}
|
||||||
searchable={true}
|
searchable={true}
|
||||||
filterable={true}
|
filterable={true}
|
||||||
sortable={true}
|
sortable={true}
|
||||||
selectable={true}
|
selectable={true}
|
||||||
onRowSelect={(rows) => setSelectedFiles(rows as UserFile[])}
|
onRowSelect={(rows) => setSelectedFiles(rows as UserFile[])}
|
||||||
rowDraggable={true}
|
rowDraggable={true}
|
||||||
onRowDragStart={_onRowDragStart}
|
onRowDragStart={_onRowDragStart}
|
||||||
getRowDataAttributes={(row: UserFile) =>
|
getRowDataAttributes={(row: UserFile) =>
|
||||||
({ highlighted: row.id === highlightedFileId ? 'true' : 'false' })
|
({ highlighted: row.id === highlightedFileId ? 'true' : 'false' })
|
||||||
}
|
}
|
||||||
actionButtons={[
|
actionButtons={[
|
||||||
...(canUpdate ? [{
|
...(canUpdate ? [{
|
||||||
type: 'edit' as const,
|
type: 'edit' as const,
|
||||||
onAction: handleEditClick,
|
onAction: handleEditClick,
|
||||||
title: t('Bearbeiten'),
|
title: t('Bearbeiten'),
|
||||||
}] : []),
|
}] : []),
|
||||||
...(canDelete ? [{
|
...(canDelete ? [{
|
||||||
type: 'delete' as const,
|
type: 'delete' as const,
|
||||||
title: t('Löschen'),
|
title: t('Löschen'),
|
||||||
loading: (row: UserFile) => deletingFiles.has(row.id),
|
loading: (row: UserFile) => deletingFiles.has(row.id),
|
||||||
}] : []),
|
}] : []),
|
||||||
]}
|
]}
|
||||||
customActions={[
|
customActions={[
|
||||||
{
|
{
|
||||||
id: 'download',
|
id: 'download',
|
||||||
icon: <FaDownload />,
|
icon: <FaDownload />,
|
||||||
onClick: handleDownload,
|
onClick: handleDownload,
|
||||||
title: t('Herunterladen'),
|
title: t('Herunterladen'),
|
||||||
loading: (row: UserFile) => downloadingFiles.has(row.id),
|
loading: (row: UserFile) => downloadingFiles.has(row.id),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'preview',
|
id: 'preview',
|
||||||
icon: <FaEye />,
|
icon: <FaEye />,
|
||||||
onClick: handlePreview,
|
onClick: handlePreview,
|
||||||
title: t('Vorschau'),
|
title: t('Vorschau'),
|
||||||
loading: (row: UserFile) => previewingFiles.has(row.id),
|
loading: (row: UserFile) => previewingFiles.has(row.id),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onDeleteMultiple={handleDeleteMultiple}
|
onDeleteMultiple={handleDeleteMultiple}
|
||||||
hookData={{
|
hookData={{
|
||||||
refetch: _tableRefetch,
|
refetch: _tableRefetch,
|
||||||
pagination,
|
pagination,
|
||||||
permissions,
|
permissions,
|
||||||
handleDelete: handleFileDelete,
|
handleDelete: handleFileDelete,
|
||||||
handleInlineUpdate,
|
handleInlineUpdate,
|
||||||
updateOptimistically: updateFileOptimistically,
|
updateOptimistically: updateFileOptimistically,
|
||||||
}}
|
}}
|
||||||
emptyMessage={t('Keine Dateien gefunden')}
|
emptyMessage={t('Keine Dateien gefunden')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Edit Modal */}
|
|
||||||
{editingFile && (
|
{editingFile && (
|
||||||
<div className={styles.modalOverlay} onClick={() => setEditingFile(null)}>
|
<div className={styles.modalOverlay} onClick={() => setEditingFile(null)}>
|
||||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,8 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
|
||||||
label: t('Erstellt von'),
|
label: t('Erstellt von'),
|
||||||
type: 'string',
|
type: 'string',
|
||||||
width: 140,
|
width: 140,
|
||||||
|
fkSource: '/api/users/',
|
||||||
|
fkDisplayField: 'username',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'sysCreatedAt',
|
key: 'sysCreatedAt',
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ const ConfigTab: React.FC = () => {
|
||||||
const PlaygroundTab: React.FC = () => {
|
const PlaygroundTab: React.FC = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const { showSuccess, showError } = useToast();
|
const { showSuccess, showError } = useToast();
|
||||||
const { refetch: refetchFiles, handleFileDownload } = useFileContext();
|
const { refreshTreeFiles: refetchFiles, handleFileDownload } = useFileContext();
|
||||||
const { connections } = useConnections();
|
const { connections } = useConnections();
|
||||||
|
|
||||||
const msftConnections = connections.filter(
|
const msftConnections = connections.filter(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue