fixed udb issues

This commit is contained in:
ValueOn AG 2026-04-12 10:12:01 +02:00
parent dfb4c5ebd7
commit 0f551423b2
6 changed files with 184 additions and 160 deletions

View file

@ -6,11 +6,11 @@
.treeNode { .treeNode {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 4px 8px; padding: 2px 4px;
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 4px;
gap: 6px; gap: 2px;
min-height: 32px; min-height: 26px;
position: relative; position: relative;
} }
@ -43,15 +43,15 @@
} }
.chevron { .chevron {
width: 16px; width: 12px;
height: 16px; height: 12px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
transition: transform 0.15s ease; transition: transform 0.15s ease;
color: var(--color-text-secondary, #666); color: var(--color-text-secondary, #666);
font-size: 10px; font-size: 8px;
} }
.chevron.expanded { .chevron.expanded {
@ -65,7 +65,7 @@
.folderIcon { .folderIcon {
flex-shrink: 0; flex-shrink: 0;
color: var(--color-text-secondary, #888); color: var(--color-text-secondary, #888);
font-size: 14px; font-size: 13px;
} }
.folderName { .folderName {
@ -120,7 +120,7 @@
} }
.children { .children {
padding-left: 16px; padding-left: 10px;
} }
.rootLabel { .rootLabel {
@ -139,7 +139,7 @@
.fileIcon { .fileIcon {
flex-shrink: 0; flex-shrink: 0;
font-size: 12px; font-size: 11px;
} }
.fileSize { .fileSize {

View file

@ -12,7 +12,7 @@
*/ */
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import { FaFolder, FaFolderOpen, FaPlus, FaPen, FaTrash, FaChevronRight, FaGlobe, FaSyncAlt, FaDownload } from 'react-icons/fa'; import { FaFolder, FaFolderOpen, FaPlus, FaPen, FaTrash, FaChevronRight, FaSyncAlt, FaDownload } from 'react-icons/fa';
import { usePrompt, type PromptOptions } from '../../hooks/usePrompt'; import { usePrompt, type PromptOptions } from '../../hooks/usePrompt';
import styles from './FolderTree.module.css'; import styles from './FolderTree.module.css';
@ -26,6 +26,9 @@ export interface FolderNode {
parentId: string | null; parentId: string | null;
fileCount?: number; fileCount?: number;
children?: FolderNode[]; children?: FolderNode[];
isProtected?: boolean;
isReadonly?: boolean;
icon?: string;
} }
export interface FileNode { export interface FileNode {
@ -36,6 +39,8 @@ export interface FileNode {
folderId?: string | null; folderId?: string | null;
scope?: string; scope?: string;
neutralize?: boolean; neutralize?: boolean;
sysCreatedBy?: string;
isReadonly?: boolean;
} }
export interface TreeItem { export interface TreeItem {
@ -236,6 +241,7 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
}} }}
onDragEnd={() => setDragging(false)} onDragEnd={() => setDragging(false)}
> >
<span className={styles.chevron} style={{ visibility: 'hidden' }}><FaChevronRight /></span>
<span className={styles.fileIcon}>{_fileIcon(file.mimeType)}</span> <span className={styles.fileIcon}>{_fileIcon(file.mimeType)}</span>
{renaming ? ( {renaming ? (
<input <input
@ -458,20 +464,26 @@ function _TreeNode({
dragging ? styles.dragging : '', dragging ? styles.dragging : '',
].filter(Boolean).join(' '); ].filter(Boolean).join(' ');
const isProtected = node.isProtected === true;
const isReadonly = node.isReadonly === true;
const notDraggable = isProtected || isReadonly;
const notEditable = isProtected || isReadonly;
const customIcon = node.icon;
return ( return (
<div> <div>
<div <div
className={nodeClasses} className={nodeClasses}
onClick={(e) => sel.onItemClick(node.id, 'folder', e)} onClick={(e) => sel.onItemClick(node.id, 'folder', e)}
draggable draggable={!notDraggable}
onDragStart={(e) => { onDragStart={notDraggable ? undefined : (e) => {
sel.onItemDragStart(e, node.id, 'folder', node.name); sel.onItemDragStart(e, node.id, 'folder', node.name);
setDragging(true); setDragging(true);
}} }}
onDragEnd={() => setDragging(false)} onDragEnd={notDraggable ? undefined : () => setDragging(false)}
onDragOver={_handleDragOver} onDragOver={isProtected ? undefined : _handleDragOver}
onDragLeave={_handleDragLeave} onDragLeave={isProtected ? undefined : _handleDragLeave}
onDrop={_handleDrop} onDrop={isProtected ? undefined : _handleDrop}
> >
<span <span
className={`${styles.chevron} ${isExpanded ? styles.expanded : ''} ${!hasChildren ? styles.empty : ''}`} className={`${styles.chevron} ${isExpanded ? styles.expanded : ''} ${!hasChildren ? styles.empty : ''}`}
@ -480,9 +492,11 @@ function _TreeNode({
<FaChevronRight /> <FaChevronRight />
</span> </span>
<span className={styles.folderIcon}> <span className={styles.folderIcon}>
{isExpanded ? <FaFolderOpen /> : <FaFolder />} {customIcon ? (
<span style={{ fontSize: 14 }}>{customIcon}</span>
) : isExpanded ? <FaFolderOpen /> : <FaFolder />}
</span> </span>
{renaming ? ( {renaming && !notEditable ? (
<input <input
ref={inputRef} ref={inputRef}
className={styles.renameInput} className={styles.renameInput}
@ -496,10 +510,11 @@ function _TreeNode({
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
/> />
) : ( ) : (
<span className={styles.folderName}>{node.name}</span> <span className={styles.folderName} style={notEditable ? { fontWeight: 600 } : undefined}>{node.name}</span>
)} )}
{!isProtected && (
<span className={styles.actions}> <span className={styles.actions}>
{onDownloadFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && ( {!notEditable && onDownloadFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onDownloadFolder(node.id, node.name); }} title={t('Ordner herunterladen (ZIP)')}> <button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onDownloadFolder(node.id, node.name); }} title={t('Ordner herunterladen (ZIP)')}>
<FaDownload /> <FaDownload />
</button> </button>
@ -509,7 +524,7 @@ function _TreeNode({
<FaPlus /> <FaPlus />
</button> </button>
)} )}
{onRenameFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && ( {!notEditable && onRenameFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); setRenameValue(node.name); setRenaming(true); }} title={t('Umbenennen')}> <button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); setRenameValue(node.name); setRenaming(true); }} title={t('Umbenennen')}>
<FaPen /> <FaPen />
</button> </button>
@ -529,12 +544,13 @@ function _TreeNode({
</button> </button>
)} )}
</> </>
) : onDeleteFolder && ( ) : !notEditable && onDeleteFolder && (
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteSingle} title={t('Löschen')}> <button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteSingle} title={t('Löschen')}>
<FaTrash /> <FaTrash />
</button> </button>
)} )}
</span> </span>
)}
</div> </div>
{isExpanded && hasChildren && ( {isExpanded && hasChildren && (
<div className={styles.children}> <div className={styles.children}>
@ -581,19 +597,38 @@ export default function FolderTree({
onScopeChange, onNeutralizeToggle, onScopeChange, onNeutralizeToggle,
}: FolderTreeProps) { }: FolderTreeProps) {
const { t } = useLanguage(); const { t } = useLanguage();
const [internalExpandedIds, setInternalExpandedIds] = useState<Set<string>>(new Set()); const [internalExpandedIds, setInternalExpandedIds] = useState<Set<string>>(new Set());
const [rootDropOver, setRootDropOver] = useState(false);
const [internalSelectedIds, setInternalSelectedIds] = useState<Set<string>>(new Set()); const [internalSelectedIds, setInternalSelectedIds] = useState<Set<string>>(new Set());
const lastClickedIdRef = useRef<string | null>(null); const lastClickedIdRef = useRef<string | null>(null);
const { prompt: promptFolderName, PromptDialog } = usePrompt(); const { prompt: promptFolderName, PromptDialog } = usePrompt();
const [rootDropOver, setRootDropOver] = useState(false);
const expandedIds = externalExpandedIds ?? internalExpandedIds; const expandedIds = externalExpandedIds ?? internalExpandedIds;
const selectedItemIds = externalSelectedIds ?? internalSelectedIds;
const tree = useMemo(() => _buildTree(folders), [folders]); const realTree = useMemo(() => _buildTree(folders), [folders]);
const filesByFolder = useMemo(() => _groupFilesByFolder(files || []), [files]); const filesByFolder = useMemo(() => _groupFilesByFolder(files || []), [files]);
const rootFiles = showFiles ? (filesByFolder.get('') || []) : []; const rootFiles = showFiles ? (filesByFolder.get('') || []) : [];
const selectedItemIds = externalSelectedIds ?? internalSelectedIds; const knownFolderIds = useMemo(() => {
const ids = new Set<string>();
const _collect = (nodes: FolderNode[]) => { for (const n of nodes) { ids.add(n.id); if (n.children) _collect(n.children); } };
_collect(realTree);
return ids;
}, [realTree]);
const tree = useMemo(() => {
if (!showFiles) return realTree;
const orphanFolders: FolderNode[] = [];
for (const key of filesByFolder.keys()) {
if (key && !knownFolderIds.has(key)) {
orphanFolders.push({ id: key, name: key.slice(0, 8) + '…', parentId: null, fileCount: filesByFolder.get(key)?.length ?? 0, isProtected: true });
}
}
if (orphanFolders.length === 0) return realTree;
return [...realTree, ...orphanFolders.sort((a, b) => a.name.localeCompare(b.name))];
}, [realTree, showFiles, filesByFolder, knownFolderIds]);
const flatList = useMemo( const flatList = useMemo(
() => _computeFlatList(tree, expandedIds, showFiles, filesByFolder), () => _computeFlatList(tree, expandedIds, showFiles, filesByFolder),
@ -703,74 +738,63 @@ export default function FolderTree({
}; };
}, [selectedItemIds, allFileIds, allFolderIds, _handleItemClick, _handleItemDragStart, onRenameFile, onDeleteFile, onDeleteFiles, onDeleteFolders, onScopeChange, onNeutralizeToggle]); }, [selectedItemIds, allFileIds, allFolderIds, _handleItemClick, _handleItemDragStart, onRenameFile, onDeleteFile, onDeleteFiles, onDeleteFolders, onScopeChange, onNeutralizeToggle]);
// Root drop handler: items dropped on the empty area go to root (null)
const _handleRootDrop = useCallback(async (e: React.DragEvent) => { const _handleRootDrop = useCallback(async (e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
setRootDropOver(false); setRootDropOver(false);
const treeItemsJson = e.dataTransfer.getData('application/tree-items'); const treeItemsJson = e.dataTransfer.getData('application/tree-items');
if (treeItemsJson) { if (treeItemsJson) {
const items: TreeItem[] = JSON.parse(treeItemsJson); const items: TreeItem[] = JSON.parse(treeItemsJson);
const fileIds = items.filter(i => i.type === 'file').map(i => i.id); const fileIds = items.filter(i => i.type === 'file').map(i => i.id);
const folderIds = items.filter(i => i.type === 'folder').map(i => i.id); const folderIds = items.filter(i => i.type === 'folder').map(i => i.id);
if (folderIds.length > 0 && onMoveFolders) { if (folderIds.length > 0 && onMoveFolders) await onMoveFolders(folderIds, null);
await onMoveFolders(folderIds, null); else if (onMoveFolder) for (const fId of folderIds) await onMoveFolder(fId, null);
} else if (onMoveFolder) { if (fileIds.length > 0 && onMoveFiles) await onMoveFiles(fileIds, null);
for (const fId of folderIds) await onMoveFolder(fId, null); else if (onMoveFile) for (const fId of fileIds) await onMoveFile(fId, null);
}
if (fileIds.length > 0 && onMoveFiles) {
await onMoveFiles(fileIds, null);
} else if (fileIds.length > 0 && onMoveFile) {
for (const fId of fileIds) await onMoveFile(fId, null);
}
return; return;
} }
const folderId = e.dataTransfer.getData('application/folder-id'); const folderId = e.dataTransfer.getData('application/folder-id');
const fileIdsJson = e.dataTransfer.getData('application/file-ids');
const fileId = e.dataTransfer.getData('application/file-id'); const fileId = e.dataTransfer.getData('application/file-id');
if (folderId && onMoveFolder) { if (folderId && onMoveFolder) await onMoveFolder(folderId, null);
await onMoveFolder(folderId, null); else if (fileId && onMoveFile) await onMoveFile(fileId, null);
} else if (fileIdsJson && onMoveFiles) {
await onMoveFiles(JSON.parse(fileIdsJson), null);
} else if (fileId && onMoveFile) {
await onMoveFile(fileId, null);
}
}, [onMoveFolder, onMoveFolders, onMoveFile, onMoveFiles]); }, [onMoveFolder, onMoveFolders, onMoveFile, onMoveFiles]);
const rootClasses = [ const _handleRootAddFolder = useCallback(async () => {
styles.treeNode, if (!onCreateFolder) return;
selectedFolderId === null ? styles.selected : '', const name = await promptFolderName(t('Neuer Ordnername:'), { title: t('Neuer Ordner'), placeholder: t('Ordnername') });
rootDropOver ? styles.dropTarget : '', if (name?.trim()) await onCreateFolder(name.trim(), null);
].filter(Boolean).join(' '); }, [onCreateFolder, promptFolderName, t]);
const isRootSelected = selectedFolderId === null;
const _handleRootClick = useCallback(() => {
_setSelection(new Set());
onSelect(null);
}, [_setSelection, onSelect]);
return ( return (
<div className={styles.folderTree}> <div className={styles.folderTree}>
<div <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '2px 4px' }}>
className={rootClasses} <span
onClick={() => { onSelect(null); _setSelection(new Set()); }} className={`${styles.treeNode} ${isRootSelected ? styles.selected : ''} ${rootDropOver ? styles.dropTarget : ''}`}
onDragOver={(e) => { e.preventDefault(); setRootDropOver(true); }} style={{ flex: 1, cursor: 'pointer', fontWeight: 600, paddingLeft: 4 }}
onClick={_handleRootClick}
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setRootDropOver(true); }}
onDragLeave={() => setRootDropOver(false)} onDragLeave={() => setRootDropOver(false)}
onDrop={_handleRootDrop} onDrop={_handleRootDrop}
> >
<span className={styles.folderIcon}><FaGlobe /></span> /
<span className={`${styles.folderName} ${styles.rootLabel}`}>({t('Global')})</span> </span>
<span className={styles.rootActions}> <span className={styles.actions}>
{onRefresh && ( {onCreateFolder && (
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onRefresh(); }} title={t('Aktualisieren')}> <button className={styles.actionBtn} onClick={_handleRootAddFolder} title={t('Neuer Ordner')}>
<FaSyncAlt /> <FaPlus />
</button> </button>
)} )}
{onCreateFolder && ( {onRefresh && (
<button <button className={styles.actionBtn} onClick={onRefresh} title={t('Aktualisieren')}>
className={styles.actionBtn} <FaSyncAlt />
onClick={async (e) => {
e.stopPropagation();
const name = await promptFolderName(t('Neuer Ordnername:'), { title: t('Neuer Ordner'), placeholder: t('Ordnername') });
if (name?.trim()) await onCreateFolder(name.trim(), null);
}}
title={t('Neuer Ordner')}
>
<FaPlus />
</button> </button>
)} )}
</span> </span>
@ -780,7 +804,7 @@ export default function FolderTree({
<_TreeNode <_TreeNode
key={node.id} key={node.id}
node={node} node={node}
depth={1} depth={0}
selectedFolderId={selectedFolderId} selectedFolderId={selectedFolderId}
expandedIds={expandedIds} expandedIds={expandedIds}
showFiles={showFiles} showFiles={showFiles}

View file

@ -39,15 +39,14 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
handleDownloadFolder, handleDownloadFolder,
} = useFileContext(); } = useFileContext();
const _folderNodes = useMemo(() => const _folderNodes = useMemo(() => {
folders.map(f => ({ return 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, fileCount: f.fileCount ?? 0,
})), }));
[folders], }, [folders]);
);
const _fileNodes: FileNode[] = useMemo(() => { const _fileNodes: FileNode[] = useMemo(() => {
let result = treeFileNodes; let result = treeFileNodes;

View file

@ -40,7 +40,7 @@ interface FileContextType {
export const FileContext = createContext<FileContextType | undefined>(undefined); export const FileContext = createContext<FileContextType | undefined>(undefined);
const _ROOT_KEY = '__root__'; const _ROOT_KEY = '';
function _toFileNode(f: any): FileNode { function _toFileNode(f: any): FileNode {
return { return {
@ -51,6 +51,7 @@ function _toFileNode(f: any): FileNode {
folderId: f.folderId ?? null, folderId: f.folderId ?? null,
scope: f.scope, scope: f.scope,
neutralize: f.neutralize, neutralize: f.neutralize,
sysCreatedBy: f.sysCreatedBy,
}; };
} }
@ -75,18 +76,19 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
}, [location.pathname]); }, [location.pathname]);
// ── Folder expanded state (persisted per feature-instance in sessionStorage) ── // ── Folder expanded state (persisted per feature-instance in sessionStorage) ──
const [expandedFolderIds, setExpandedFolderIds] = useState<Set<string>>(() => { const _loadExpanded = (key: string): Set<string> => {
try { try {
const stored = sessionStorage.getItem(storageKey); const stored = sessionStorage.getItem(key);
return stored ? new Set<string>(JSON.parse(stored)) : new Set<string>(); if (!stored) return new Set<string>();
const ids: string[] = JSON.parse(stored);
return new Set(ids.filter(id => id && id !== '__root__'));
} catch { return new Set<string>(); } } catch { return new Set<string>(); }
}); };
const [expandedFolderIds, setExpandedFolderIds] = useState<Set<string>>(() => _loadExpanded(storageKey));
useEffect(() => { useEffect(() => {
try { setExpandedFolderIds(_loadExpanded(storageKey));
const stored = sessionStorage.getItem(storageKey);
setExpandedFolderIds(stored ? new Set<string>(JSON.parse(stored)) : new Set<string>());
} catch { setExpandedFolderIds(new Set<string>()); }
}, [storageKey]); }, [storageKey]);
// ── Folder state ────────────────────────────────────────────────────── // ── Folder state ──────────────────────────────────────────────────────
@ -150,6 +152,7 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
const refreshTreeFiles = useCallback(async () => { const refreshTreeFiles = useCallback(async () => {
const keys = Array.from(treeFilesMap.keys()); const keys = Array.from(treeFilesMap.keys());
if (!keys.includes(_ROOT_KEY)) keys.push(_ROOT_KEY);
await Promise.all( await Promise.all(
keys.map(key => loadTreeFiles(key === _ROOT_KEY ? '' : key)), keys.map(key => loadTreeFiles(key === _ROOT_KEY ? '' : key)),
); );
@ -183,7 +186,6 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
const next = new Set(prev); const next = new Set(prev);
if (next.has(id)) { if (next.has(id)) {
next.delete(id); next.delete(id);
_removeTreeFiles(id);
} else { } else {
next.add(id); next.add(id);
loadTreeFiles(id); loadTreeFiles(id);
@ -191,7 +193,7 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
try { sessionStorage.setItem(storageKey, JSON.stringify([...next])); } catch {} try { sessionStorage.setItem(storageKey, JSON.stringify([...next])); } catch {}
return next; return next;
}); });
}, [storageKey, loadTreeFiles, _removeTreeFiles]); }, [storageKey, loadTreeFiles]);
// ── Folder operations ───────────────────────────────────────────────── // ── Folder operations ─────────────────────────────────────────────────
const handleCreateFolder = useCallback(async (name: string, parentId: string | null) => { const handleCreateFolder = useCallback(async (name: string, parentId: string | null) => {

View file

@ -544,27 +544,12 @@ export function useFileOperations() {
} }
}; };
const handleFileUpdate = async (fileId: string, updateData: Partial<{ fileName: string }>, originalFileData?: any) => { const handleFileUpdate = async (fileId: string, updateData: Record<string, any>, _originalFileData?: any) => {
setUploadError(null); // Reuse upload error state for update operations setUploadError(null);
setEditingFiles(prev => new Set(prev).add(fileId)); setEditingFiles(prev => new Set(prev).add(fileId));
try { try {
// Use PUT request with complete file object const updatedFile = await updateFileApi(request, fileId, updateData);
// Always use current timestamp for creationDate to avoid validation issues
const currentTimestamp = Math.floor(Date.now() / 1000);
const creationDate = currentTimestamp;
const completeFileObject = {
id: fileId,
mandateId: originalFileData?.mandateId || "00000000-0000-0000-0000-000000000000",
fileName: updateData.fileName,
mimeType: originalFileData?.mime_type || "application/octet-stream",
fileHash: originalFileData?.fileHash || "0000000000000000000000000000000000000000",
fileSize: originalFileData?.size || 0,
creationDate: Math.floor(creationDate) // Ensure it's an integer
};
const updatedFile = await updateFileApi(request, fileId, completeFileObject);
return { success: true, fileData: updatedFile }; return { success: true, fileData: updatedFile };
} catch (error: any) { } catch (error: any) {
@ -934,13 +919,8 @@ export function useFileOperations() {
} }
}; };
// Generic inline update handler for FormGeneratorTable const handleInlineUpdate = async (fileId: string, changes: Record<string, any>, _existingRow?: any) => {
const handleInlineUpdate = async (fileId: string, changes: Partial<{ fileName: string }>, existingRow?: any) => { const result = await handleFileUpdate(fileId, changes);
if (!existingRow) {
throw new Error('Existing row data required for inline update');
}
const result = await handleFileUpdate(fileId, changes, existingRow);
if (!result.success) { if (!result.success) {
throw new Error(result.error || 'Failed to update'); throw new Error(result.error || 'Failed to update');
} }

View file

@ -19,6 +19,7 @@ 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';
import { getUserDataCache } from '../../utils/userCache';
interface UserFile { interface UserFile {
id: string; id: string;
@ -96,11 +97,17 @@ 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);
// ── Table refetch: always includes folderId filter ──────────────────── // ── Table refetch: filter by real folderId ───────────────────────────
const _tableRefetch = useCallback(async (params?: any) => { const _tableRefetch = useCallback(async (params?: any) => {
const nextParams = { ...(params || {}) }; const nextParams = { ...(params || {}) };
const nextFilters = { ...(nextParams.filters || {}) }; const nextFilters = { ...(nextParams.filters || {}) };
if (!selectedFolderId) {
nextFilters.folderId = null;
} else {
nextFilters.folderId = selectedFolderId; nextFilters.folderId = selectedFolderId;
}
nextParams.filters = nextFilters; nextParams.filters = nextFilters;
await tableRefetch(nextParams); await tableRefetch(nextParams);
}, [tableRefetch, selectedFolderId]); }, [tableRefetch, selectedFolderId]);
@ -113,16 +120,15 @@ export const FilesPage: React.FC = () => {
await Promise.all([_tableRefetch(), refreshTreeFiles(), refreshFolders()]); await Promise.all([_tableRefetch(), refreshTreeFiles(), refreshFolders()]);
}, [_tableRefetch, refreshTreeFiles, refreshFolders]); }, [_tableRefetch, refreshTreeFiles, refreshFolders]);
// ── Folder nodes for tree (with fileCount) ──────────────────────────── // ── Folder nodes for tree (real folders only) ────────────────────────
const folderNodes = useMemo(() => const folderNodes = useMemo(() => {
folders.map(f => ({ return 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, fileCount: f.fileCount ?? 0,
})), }));
[folders], }, [folders]);
);
// ── Columns ─────────────────────────────────────────────────────────── // ── Columns ───────────────────────────────────────────────────────────
const columns = useMemo(() => { const columns = useMemo(() => {
@ -162,6 +168,9 @@ 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';
const currentUserId = useMemo(() => getUserDataCache()?.id || '', []);
const _isOwned = useCallback((row: UserFile) => row.sysCreatedBy === currentUserId, [currentUserId]);
// ── Tree event handlers ─────────────────────────────────────────────── // ── Tree event handlers ───────────────────────────────────────────────
const _handleTreeFileSelect = useCallback((fileId: string) => { const _handleTreeFileSelect = useCallback((fileId: string) => {
const file = treeFileNodes.find(f => f.id === fileId); const file = treeFileNodes.find(f => f.id === fileId);
@ -214,9 +223,17 @@ export const FilesPage: React.FC = () => {
const handleEditSubmit = async (data: Partial<UserFile>) => { const handleEditSubmit = async (data: Partial<UserFile>) => {
if (!editingFile) return; if (!editingFile) return;
const result = await handleFileUpdate(editingFile.id, { const changes: Record<string, any> = {};
fileName: data.fileName || editingFile.fileName const editableFields = ['fileName', 'scope', 'tags', 'description', 'folderId', 'neutralize'] as const;
}, editingFile); for (const field of editableFields) {
if (data[field] !== undefined && data[field] !== editingFile[field]) {
changes[field] = data[field];
}
}
if (Object.keys(changes).length === 0 && data.fileName) {
changes.fileName = data.fileName;
}
const result = await handleFileUpdate(editingFile.id, changes);
if (result.success) { if (result.success) {
setEditingFile(null); setEditingFile(null);
await Promise.all([_tableRefetch(), refreshTreeFiles()]); await Promise.all([_tableRefetch(), refreshTreeFiles()]);
@ -427,11 +444,13 @@ export const FilesPage: React.FC = () => {
type: 'edit' as const, type: 'edit' as const,
onAction: handleEditClick, onAction: handleEditClick,
title: t('Bearbeiten'), title: t('Bearbeiten'),
disabled: (row: UserFile) => !_isOwned(row) ? { disabled: true, message: t('Nur Eigentümer kann bearbeiten') } : false,
}] : []), }] : []),
...(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),
disabled: (row: UserFile) => !_isOwned(row) ? { disabled: true, message: t('Nur Eigentümer kann löschen') } : false,
}] : []), }] : []),
]} ]}
customActions={[ customActions={[