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 {
|
.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 {
|
||||||
|
|
|
||||||
|
|
@ -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,45 +510,47 @@ 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}>
|
||||||
|
{!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>
|
</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 }}
|
||||||
onDragLeave={() => setRootDropOver(false)}
|
onClick={_handleRootClick}
|
||||||
onDrop={_handleRootDrop}
|
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setRootDropOver(true); }}
|
||||||
>
|
onDragLeave={() => setRootDropOver(false)}
|
||||||
<span className={styles.folderIcon}><FaGlobe /></span>
|
onDrop={_handleRootDrop}
|
||||||
<span className={`${styles.folderName} ${styles.rootLabel}`}>({t('Global')})</span>
|
>
|
||||||
<span className={styles.rootActions}>
|
/
|
||||||
{onRefresh && (
|
</span>
|
||||||
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onRefresh(); }} title={t('Aktualisieren')}>
|
<span className={styles.actions}>
|
||||||
<FaSyncAlt />
|
{onCreateFolder && (
|
||||||
|
<button className={styles.actionBtn} onClick={_handleRootAddFolder} title={t('Neuer Ordner')}>
|
||||||
|
<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}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 || {}) };
|
||||||
nextFilters.folderId = selectedFolderId;
|
|
||||||
|
if (!selectedFolderId) {
|
||||||
|
nextFilters.folderId = null;
|
||||||
|
} else {
|
||||||
|
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={[
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue