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

View file

@ -12,7 +12,7 @@
*/
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 styles from './FolderTree.module.css';
@ -26,6 +26,9 @@ export interface FolderNode {
parentId: string | null;
fileCount?: number;
children?: FolderNode[];
isProtected?: boolean;
isReadonly?: boolean;
icon?: string;
}
export interface FileNode {
@ -36,6 +39,8 @@ export interface FileNode {
folderId?: string | null;
scope?: string;
neutralize?: boolean;
sysCreatedBy?: string;
isReadonly?: boolean;
}
export interface TreeItem {
@ -236,6 +241,7 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
}}
onDragEnd={() => setDragging(false)}
>
<span className={styles.chevron} style={{ visibility: 'hidden' }}><FaChevronRight /></span>
<span className={styles.fileIcon}>{_fileIcon(file.mimeType)}</span>
{renaming ? (
<input
@ -458,20 +464,26 @@ function _TreeNode({
dragging ? styles.dragging : '',
].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 (
<div>
<div
className={nodeClasses}
onClick={(e) => sel.onItemClick(node.id, 'folder', e)}
draggable
onDragStart={(e) => {
draggable={!notDraggable}
onDragStart={notDraggable ? undefined : (e) => {
sel.onItemDragStart(e, node.id, 'folder', node.name);
setDragging(true);
}}
onDragEnd={() => setDragging(false)}
onDragOver={_handleDragOver}
onDragLeave={_handleDragLeave}
onDrop={_handleDrop}
onDragEnd={notDraggable ? undefined : () => setDragging(false)}
onDragOver={isProtected ? undefined : _handleDragOver}
onDragLeave={isProtected ? undefined : _handleDragLeave}
onDrop={isProtected ? undefined : _handleDrop}
>
<span
className={`${styles.chevron} ${isExpanded ? styles.expanded : ''} ${!hasChildren ? styles.empty : ''}`}
@ -480,9 +492,11 @@ function _TreeNode({
<FaChevronRight />
</span>
<span className={styles.folderIcon}>
{isExpanded ? <FaFolderOpen /> : <FaFolder />}
{customIcon ? (
<span style={{ fontSize: 14 }}>{customIcon}</span>
) : isExpanded ? <FaFolderOpen /> : <FaFolder />}
</span>
{renaming ? (
{renaming && !notEditable ? (
<input
ref={inputRef}
className={styles.renameInput}
@ -496,45 +510,47 @@ function _TreeNode({
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}>
{!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)')}>
<FaDownload />
</button>
)}
{onCreateFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
<button className={styles.actionBtn} onClick={_handleAdd} title={t('Neuer Unterordner')}>
<FaPlus />
</button>
)}
{!notEditable && onRenameFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); setRenameValue(node.name); setRenaming(true); }} title={t('Umbenennen')}>
<FaPen />
</button>
)}
{isMultiSelected && sel.selectedItemIds.size > 1 ? (
<>
{sel.selectedFolderIds.length > 0 && sel.onDeleteFolders && (
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFolders} title={`${sel.selectedFolderIds.length} ${t('Ordner löschen')}`}>
<FaFolder style={{ fontSize: 8, marginRight: 1 }} /><FaTrash />
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFolderIds.length}</span>
</button>
)}
{sel.selectedFileIds.length > 0 && sel.onDeleteFiles && (
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFiles} title={`${sel.selectedFileIds.length} ${t('Dateien löschen')}`}>
<FaTrash />
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFileIds.length}</span>
</button>
)}
</>
) : !notEditable && onDeleteFolder && (
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteSingle} title={t('Löschen')}>
<FaTrash />
</button>
)}
</span>
)}
<span className={styles.actions}>
{onDownloadFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onDownloadFolder(node.id, node.name); }} title={t('Ordner herunterladen (ZIP)')}>
<FaDownload />
</button>
)}
{onCreateFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
<button className={styles.actionBtn} onClick={_handleAdd} title={t('Neuer Unterordner')}>
<FaPlus />
</button>
)}
{onRenameFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); setRenameValue(node.name); setRenaming(true); }} title={t('Umbenennen')}>
<FaPen />
</button>
)}
{isMultiSelected && sel.selectedItemIds.size > 1 ? (
<>
{sel.selectedFolderIds.length > 0 && sel.onDeleteFolders && (
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFolders} title={`${sel.selectedFolderIds.length} ${t('Ordner löschen')}`}>
<FaFolder style={{ fontSize: 8, marginRight: 1 }} /><FaTrash />
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFolderIds.length}</span>
</button>
)}
{sel.selectedFileIds.length > 0 && sel.onDeleteFiles && (
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFiles} title={`${sel.selectedFileIds.length} ${t('Dateien löschen')}`}>
<FaTrash />
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFileIds.length}</span>
</button>
)}
</>
) : onDeleteFolder && (
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteSingle} title={t('Löschen')}>
<FaTrash />
</button>
)}
</span>
</div>
{isExpanded && hasChildren && (
<div className={styles.children}>
@ -581,19 +597,38 @@ export default function FolderTree({
onScopeChange, onNeutralizeToggle,
}: FolderTreeProps) {
const { t } = useLanguage();
const [internalExpandedIds, setInternalExpandedIds] = useState<Set<string>>(new Set());
const [rootDropOver, setRootDropOver] = useState(false);
const [internalSelectedIds, setInternalSelectedIds] = useState<Set<string>>(new Set());
const lastClickedIdRef = useRef<string | null>(null);
const { prompt: promptFolderName, PromptDialog } = usePrompt();
const [rootDropOver, setRootDropOver] = useState(false);
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 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(
() => _computeFlatList(tree, expandedIds, showFiles, filesByFolder),
@ -703,74 +738,63 @@ export default function FolderTree({
};
}, [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) => {
e.preventDefault();
setRootDropOver(false);
const treeItemsJson = e.dataTransfer.getData('application/tree-items');
if (treeItemsJson) {
const items: TreeItem[] = JSON.parse(treeItemsJson);
const fileIds = items.filter(i => i.type === 'file').map(i => i.id);
const folderIds = items.filter(i => i.type === 'folder').map(i => i.id);
if (folderIds.length > 0 && onMoveFolders) {
await onMoveFolders(folderIds, null);
} else if (onMoveFolder) {
for (const fId of folderIds) await onMoveFolder(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);
}
if (folderIds.length > 0 && onMoveFolders) await onMoveFolders(folderIds, null);
else if (onMoveFolder) for (const fId of folderIds) await onMoveFolder(fId, null);
if (fileIds.length > 0 && onMoveFiles) await onMoveFiles(fileIds, null);
else if (onMoveFile) for (const fId of fileIds) await onMoveFile(fId, null);
return;
}
const folderId = e.dataTransfer.getData('application/folder-id');
const fileIdsJson = e.dataTransfer.getData('application/file-ids');
const fileId = e.dataTransfer.getData('application/file-id');
if (folderId && onMoveFolder) {
await onMoveFolder(folderId, null);
} else if (fileIdsJson && onMoveFiles) {
await onMoveFiles(JSON.parse(fileIdsJson), null);
} else if (fileId && onMoveFile) {
await onMoveFile(fileId, null);
}
if (folderId && onMoveFolder) await onMoveFolder(folderId, null);
else if (fileId && onMoveFile) await onMoveFile(fileId, null);
}, [onMoveFolder, onMoveFolders, onMoveFile, onMoveFiles]);
const rootClasses = [
styles.treeNode,
selectedFolderId === null ? styles.selected : '',
rootDropOver ? styles.dropTarget : '',
].filter(Boolean).join(' ');
const _handleRootAddFolder = useCallback(async () => {
if (!onCreateFolder) return;
const name = await promptFolderName(t('Neuer Ordnername:'), { title: t('Neuer Ordner'), placeholder: t('Ordnername') });
if (name?.trim()) await onCreateFolder(name.trim(), null);
}, [onCreateFolder, promptFolderName, t]);
const isRootSelected = selectedFolderId === null;
const _handleRootClick = useCallback(() => {
_setSelection(new Set());
onSelect(null);
}, [_setSelection, onSelect]);
return (
<div className={styles.folderTree}>
<div
className={rootClasses}
onClick={() => { onSelect(null); _setSelection(new Set()); }}
onDragOver={(e) => { e.preventDefault(); setRootDropOver(true); }}
onDragLeave={() => setRootDropOver(false)}
onDrop={_handleRootDrop}
>
<span className={styles.folderIcon}><FaGlobe /></span>
<span className={`${styles.folderName} ${styles.rootLabel}`}>({t('Global')})</span>
<span className={styles.rootActions}>
{onRefresh && (
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onRefresh(); }} title={t('Aktualisieren')}>
<FaSyncAlt />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '2px 4px' }}>
<span
className={`${styles.treeNode} ${isRootSelected ? styles.selected : ''} ${rootDropOver ? styles.dropTarget : ''}`}
style={{ flex: 1, cursor: 'pointer', fontWeight: 600, paddingLeft: 4 }}
onClick={_handleRootClick}
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setRootDropOver(true); }}
onDragLeave={() => setRootDropOver(false)}
onDrop={_handleRootDrop}
>
/
</span>
<span className={styles.actions}>
{onCreateFolder && (
<button className={styles.actionBtn} onClick={_handleRootAddFolder} title={t('Neuer Ordner')}>
<FaPlus />
</button>
)}
{onCreateFolder && (
<button
className={styles.actionBtn}
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 />
{onRefresh && (
<button className={styles.actionBtn} onClick={onRefresh} title={t('Aktualisieren')}>
<FaSyncAlt />
</button>
)}
</span>
@ -780,7 +804,7 @@ export default function FolderTree({
<_TreeNode
key={node.id}
node={node}
depth={1}
depth={0}
selectedFolderId={selectedFolderId}
expandedIds={expandedIds}
showFiles={showFiles}

View file

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

View file

@ -40,7 +40,7 @@ interface FileContextType {
export const FileContext = createContext<FileContextType | undefined>(undefined);
const _ROOT_KEY = '__root__';
const _ROOT_KEY = '';
function _toFileNode(f: any): FileNode {
return {
@ -51,6 +51,7 @@ function _toFileNode(f: any): FileNode {
folderId: f.folderId ?? null,
scope: f.scope,
neutralize: f.neutralize,
sysCreatedBy: f.sysCreatedBy,
};
}
@ -75,18 +76,19 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
}, [location.pathname]);
// ── Folder expanded state (persisted per feature-instance in sessionStorage) ──
const [expandedFolderIds, setExpandedFolderIds] = useState<Set<string>>(() => {
const _loadExpanded = (key: string): Set<string> => {
try {
const stored = sessionStorage.getItem(storageKey);
return stored ? new Set<string>(JSON.parse(stored)) : new Set<string>();
const stored = sessionStorage.getItem(key);
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>(); }
});
};
const [expandedFolderIds, setExpandedFolderIds] = useState<Set<string>>(() => _loadExpanded(storageKey));
useEffect(() => {
try {
const stored = sessionStorage.getItem(storageKey);
setExpandedFolderIds(stored ? new Set<string>(JSON.parse(stored)) : new Set<string>());
} catch { setExpandedFolderIds(new Set<string>()); }
setExpandedFolderIds(_loadExpanded(storageKey));
}, [storageKey]);
// ── Folder state ──────────────────────────────────────────────────────
@ -150,6 +152,7 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
const refreshTreeFiles = useCallback(async () => {
const keys = Array.from(treeFilesMap.keys());
if (!keys.includes(_ROOT_KEY)) keys.push(_ROOT_KEY);
await Promise.all(
keys.map(key => loadTreeFiles(key === _ROOT_KEY ? '' : key)),
);
@ -183,7 +186,6 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
_removeTreeFiles(id);
} else {
next.add(id);
loadTreeFiles(id);
@ -191,7 +193,7 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
try { sessionStorage.setItem(storageKey, JSON.stringify([...next])); } catch {}
return next;
});
}, [storageKey, loadTreeFiles, _removeTreeFiles]);
}, [storageKey, loadTreeFiles]);
// ── Folder operations ─────────────────────────────────────────────────
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) => {
setUploadError(null); // Reuse upload error state for update operations
const handleFileUpdate = async (fileId: string, updateData: Record<string, any>, _originalFileData?: any) => {
setUploadError(null);
setEditingFiles(prev => new Set(prev).add(fileId));
try {
// Use PUT request with complete file object
// 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);
const updatedFile = await updateFileApi(request, fileId, updateData);
return { success: true, fileData: updatedFile };
} catch (error: any) {
@ -934,13 +919,8 @@ export function useFileOperations() {
}
};
// Generic inline update handler for FormGeneratorTable
const handleInlineUpdate = async (fileId: string, changes: Partial<{ fileName: string }>, existingRow?: any) => {
if (!existingRow) {
throw new Error('Existing row data required for inline update');
}
const result = await handleFileUpdate(fileId, changes, existingRow);
const handleInlineUpdate = async (fileId: string, changes: Record<string, any>, _existingRow?: any) => {
const result = await handleFileUpdate(fileId, changes);
if (!result.success) {
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 styles from '../admin/Admin.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
import { getUserDataCache } from '../../utils/userCache';
interface UserFile {
id: string;
@ -96,11 +97,17 @@ export const FilesPage: React.FC = () => {
const [treeSelectedIds, setTreeSelectedIds] = useState<Set<string>>(new Set());
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 nextParams = { ...(params || {}) };
const nextFilters = { ...(nextParams.filters || {}) };
nextFilters.folderId = selectedFolderId;
if (!selectedFolderId) {
nextFilters.folderId = null;
} else {
nextFilters.folderId = selectedFolderId;
}
nextParams.filters = nextFilters;
await tableRefetch(nextParams);
}, [tableRefetch, selectedFolderId]);
@ -113,16 +120,15 @@ export const FilesPage: React.FC = () => {
await Promise.all([_tableRefetch(), refreshTreeFiles(), refreshFolders()]);
}, [_tableRefetch, refreshTreeFiles, refreshFolders]);
// ── Folder nodes for tree (with fileCount) ────────────────────────────
const folderNodes = useMemo(() =>
folders.map(f => ({
// ── Folder nodes for tree (real folders only) ────────────────────────
const folderNodes = useMemo(() => {
return folders.map(f => ({
id: f.id,
name: f.name,
parentId: f.parentId ?? null,
fileCount: f.fileCount ?? 0,
})),
[folders],
);
}));
}, [folders]);
// ── Columns ───────────────────────────────────────────────────────────
const columns = useMemo(() => {
@ -162,6 +168,9 @@ export const FilesPage: React.FC = () => {
const canUpdate = permissions?.update !== 'n';
const canDelete = permissions?.delete !== 'n';
const currentUserId = useMemo(() => getUserDataCache()?.id || '', []);
const _isOwned = useCallback((row: UserFile) => row.sysCreatedBy === currentUserId, [currentUserId]);
// ── Tree event handlers ───────────────────────────────────────────────
const _handleTreeFileSelect = useCallback((fileId: string) => {
const file = treeFileNodes.find(f => f.id === fileId);
@ -214,9 +223,17 @@ export const FilesPage: React.FC = () => {
const handleEditSubmit = async (data: Partial<UserFile>) => {
if (!editingFile) return;
const result = await handleFileUpdate(editingFile.id, {
fileName: data.fileName || editingFile.fileName
}, editingFile);
const changes: Record<string, any> = {};
const editableFields = ['fileName', 'scope', 'tags', 'description', 'folderId', 'neutralize'] as const;
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) {
setEditingFile(null);
await Promise.all([_tableRefetch(), refreshTreeFiles()]);
@ -427,11 +444,13 @@ export const FilesPage: React.FC = () => {
type: 'edit' as const,
onAction: handleEditClick,
title: t('Bearbeiten'),
disabled: (row: UserFile) => !_isOwned(row) ? { disabled: true, message: t('Nur Eigentümer kann bearbeiten') } : false,
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: t('Löschen'),
loading: (row: UserFile) => deletingFiles.has(row.id),
disabled: (row: UserFile) => !_isOwned(row) ? { disabled: true, message: t('Nur Eigentümer kann löschen') } : false,
}] : []),
]}
customActions={[