fixed udb issues
This commit is contained in:
parent
dfb4c5ebd7
commit
0f551423b2
6 changed files with 184 additions and 160 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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={[
|
||||
|
|
|
|||
Loading…
Reference in a new issue