From 0f551423b201655f6120257245a8082c14dbc536 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Sun, 12 Apr 2026 10:12:01 +0200 Subject: [PATCH] fixed udb issues --- .../FolderTree/FolderTree.module.css | 18 +- src/components/FolderTree/FolderTree.tsx | 222 ++++++++++-------- src/components/UnifiedDataBar/FilesTab.tsx | 9 +- src/contexts/FileContext.tsx | 24 +- src/hooks/useFiles.ts | 30 +-- src/pages/basedata/FilesPage.tsx | 41 +++- 6 files changed, 184 insertions(+), 160 deletions(-) diff --git a/src/components/FolderTree/FolderTree.module.css b/src/components/FolderTree/FolderTree.module.css index 1530585..b13f1fe 100644 --- a/src/components/FolderTree/FolderTree.module.css +++ b/src/components/FolderTree/FolderTree.module.css @@ -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 { diff --git a/src/components/FolderTree/FolderTree.tsx b/src/components/FolderTree/FolderTree.tsx index 71f31c8..15a4d79 100644 --- a/src/components/FolderTree/FolderTree.tsx +++ b/src/components/FolderTree/FolderTree.tsx @@ -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)} > + {_fileIcon(file.mimeType)} {renaming ? (
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} > - {isExpanded ? : } + {customIcon ? ( + {customIcon} + ) : isExpanded ? : } - {renaming ? ( + {renaming && !notEditable ? ( e.stopPropagation()} /> ) : ( - {node.name} + {node.name} + )} + {!isProtected && ( + + {!notEditable && onDownloadFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && ( + + )} + {onCreateFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && ( + + )} + {!notEditable && onRenameFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && ( + + )} + {isMultiSelected && sel.selectedItemIds.size > 1 ? ( + <> + {sel.selectedFolderIds.length > 0 && sel.onDeleteFolders && ( + + )} + {sel.selectedFileIds.length > 0 && sel.onDeleteFiles && ( + + )} + + ) : !notEditable && onDeleteFolder && ( + + )} + )} - - {onDownloadFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && ( - - )} - {onCreateFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && ( - - )} - {onRenameFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && ( - - )} - {isMultiSelected && sel.selectedItemIds.size > 1 ? ( - <> - {sel.selectedFolderIds.length > 0 && sel.onDeleteFolders && ( - - )} - {sel.selectedFileIds.length > 0 && sel.onDeleteFiles && ( - - )} - - ) : onDeleteFolder && ( - - )} -
{isExpanded && hasChildren && (
@@ -581,19 +597,38 @@ export default function FolderTree({ onScopeChange, onNeutralizeToggle, }: FolderTreeProps) { const { t } = useLanguage(); + const [internalExpandedIds, setInternalExpandedIds] = useState>(new Set()); - const [rootDropOver, setRootDropOver] = useState(false); const [internalSelectedIds, setInternalSelectedIds] = useState>(new Set()); const lastClickedIdRef = useRef(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(); + 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 (
-
{ onSelect(null); _setSelection(new Set()); }} - onDragOver={(e) => { e.preventDefault(); setRootDropOver(true); }} - onDragLeave={() => setRootDropOver(false)} - onDrop={_handleRootDrop} - > - - ({t('Global')}) - - {onRefresh && ( - )} - {onCreateFolder && ( - )} @@ -780,7 +804,7 @@ export default function FolderTree({ <_TreeNode key={node.id} node={node} - depth={1} + depth={0} selectedFolderId={selectedFolderId} expandedIds={expandedIds} showFiles={showFiles} diff --git a/src/components/UnifiedDataBar/FilesTab.tsx b/src/components/UnifiedDataBar/FilesTab.tsx index 90ade97..0f52574 100644 --- a/src/components/UnifiedDataBar/FilesTab.tsx +++ b/src/components/UnifiedDataBar/FilesTab.tsx @@ -39,15 +39,14 @@ const FilesTab: React.FC = ({ 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; diff --git a/src/contexts/FileContext.tsx b/src/contexts/FileContext.tsx index d148a6e..8ddf8ae 100644 --- a/src/contexts/FileContext.tsx +++ b/src/contexts/FileContext.tsx @@ -40,7 +40,7 @@ interface FileContextType { export const FileContext = createContext(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>(() => { + const _loadExpanded = (key: string): Set => { try { - const stored = sessionStorage.getItem(storageKey); - return stored ? new Set(JSON.parse(stored)) : new Set(); + const stored = sessionStorage.getItem(key); + if (!stored) return new Set(); + const ids: string[] = JSON.parse(stored); + return new Set(ids.filter(id => id && id !== '__root__')); } catch { return new Set(); } - }); + }; + + const [expandedFolderIds, setExpandedFolderIds] = useState>(() => _loadExpanded(storageKey)); useEffect(() => { - try { - const stored = sessionStorage.getItem(storageKey); - setExpandedFolderIds(stored ? new Set(JSON.parse(stored)) : new Set()); - } catch { setExpandedFolderIds(new Set()); } + 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) => { diff --git a/src/hooks/useFiles.ts b/src/hooks/useFiles.ts index d3e3250..305c3d1 100644 --- a/src/hooks/useFiles.ts +++ b/src/hooks/useFiles.ts @@ -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, _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, _existingRow?: any) => { + const result = await handleFileUpdate(fileId, changes); if (!result.success) { throw new Error(result.error || 'Failed to update'); } diff --git a/src/pages/basedata/FilesPage.tsx b/src/pages/basedata/FilesPage.tsx index 7acd112..d3bb5dc 100644 --- a/src/pages/basedata/FilesPage.tsx +++ b/src/pages/basedata/FilesPage.tsx @@ -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>(new Set()); const [highlightedFileId, setHighlightedFileId] = useState(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) => { if (!editingFile) return; - const result = await handleFileUpdate(editingFile.id, { - fileName: data.fileName || editingFile.fileName - }, editingFile); + const changes: Record = {}; + 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={[