frontend: folders

This commit is contained in:
2025-10-27 01:31:06 +01:00
parent 264316eb29
commit b260065245
3 changed files with 317 additions and 1 deletions
+209 -1
View File
@@ -594,6 +594,7 @@ const AppLayout = () => {
const selectionInitializedRef = useRef(false);
const dragCounterRef = useRef(0);
const prefetchedFoldersRef = useRef(new Set(['root']));
const detailFolderFetchRef = useRef(new Set());
const selectionAnchorRef = useRef(initialRowSelection[initialRowSelection.length - 1] || null);
const selectionOrderRef = useRef(initialRowSelection);
@@ -643,6 +644,7 @@ const AppLayout = () => {
dragCounterRef.current = 0;
prefetchedFoldersRef.current = new Set(['root']);
breadcrumbFetchRef.current = new Set();
detailFolderFetchRef.current = new Set();
bootstrapInitializedRef.current = false;
selectionInitializedRef.current = false;
}, [assetManager]);
@@ -1868,19 +1870,134 @@ const AppLayout = () => {
greedyPrefetchFolders(pendingIds).catch(() => {});
}, [folderContents, greedyPrefetchFolders]);
const expandFolderAncestors = useCallback(
(targetId) => {
if (!targetId || targetId === 'root') {
setFolderNodes((prev) => {
if (prev.get('root')?.expanded) {
return prev;
}
const next = new Map(prev);
const rootNode = next.get('root');
if (rootNode) {
next.set('root', { ...rootNode, expanded: true });
}
return next;
});
return;
}
setFolderNodes((prev) => {
const next = new Map(prev);
let currentId = targetId;
let guard = 0;
while (currentId && !next.has(currentId) && guard < 32) {
guard += 1;
const node = prev.get(currentId);
if (!node) {
break;
}
currentId = node.parentId ?? 'root';
}
currentId = targetId;
guard = 0;
while (currentId && guard < 32) {
guard += 1;
const node = next.get(currentId);
if (!node) {
break;
}
if (!node.expanded && currentId !== targetId) {
next.set(currentId, { ...node, expanded: true });
}
currentId = node.parentId ?? 'root';
if (!currentId || currentId === 'root') {
const rootNode = next.get('root');
if (rootNode && !rootNode.expanded) {
next.set('root', { ...rootNode, expanded: true });
}
break;
}
}
return next;
});
},
[]);
const ensureFolderAncestorsLoaded = useCallback(
async (targetId) => {
if (!targetId || targetId === 'root') {
return;
}
const visited = new Set();
const stack = [];
let currentId = targetId;
let guard = 0;
while (currentId && currentId !== 'root' && guard < 32) {
guard += 1;
if (visited.has(currentId)) {
break;
}
visited.add(currentId);
const node = folderNodes.get(currentId);
if (!node) {
stack.push(currentId);
break;
}
const parentId = node.parentId ?? 'root';
if (!parentId || parentId === 'root') {
break;
}
stack.push(parentId);
currentId = parentId;
}
while (stack.length) {
const ancestorId = stack.pop();
if (!ancestorId || ancestorId === 'root') {
continue;
}
if (!folderNodes.has(ancestorId)) {
try {
await ensureFolderData(ancestorId, { includeDocuments: false, force: false });
} catch (error) {
console.warn('Failed to ensure ancestor folder for navigation', ancestorId, error);
}
}
}
},
[folderNodes, ensureFolderData],
);
const selectFolder = useCallback(
async (folderId, { replace = false, immediate = false } = {}) => {
const targetId = folderId && folderId !== 'root' ? folderId : 'root';
await ensureFolderAncestorsLoaded(targetId);
expandFolderAncestors(targetId);
if (!navigate || immediate) {
await loadFolder(targetId, { preserveSearch: isFilterActive });
setSelectedFolder(targetId);
return;
}
const path = targetId === 'root' ? '/documents' : `/documents/folder/${targetId}`;
navigate(path, { replace });
},
[loadFolder, navigate, isFilterActive],
[
ensureFolderAncestorsLoaded,
expandFolderAncestors,
navigate,
loadFolder,
isFilterActive,
setSelectedFolder,
],
);
const initializeAfterLogin = useCallback(async () => {
@@ -4450,6 +4567,95 @@ const AppLayout = () => {
});
}, [missingBreadcrumbAncestors, ensureFolderData]);
useEffect(() => {
if (!orderedSelectedDocuments.length) {
return;
}
const visited = new Set();
orderedSelectedDocuments.forEach((doc) => {
const folderId = doc?.folder_id;
if (!folderId) {
return;
}
let currentId = folderId;
let guard = 0;
while (currentId && currentId !== 'root' && guard < 32) {
guard += 1;
if (visited.has(currentId)) {
break;
}
visited.add(currentId);
const node = folderNodes.get(currentId);
if (!node) {
if (!detailFolderFetchRef.current.has(currentId)) {
detailFolderFetchRef.current.add(currentId);
ensureFolderData(currentId, { force: false, includeDocuments: false })
.catch((error) => {
console.warn('Failed to preload folder metadata for detail path', currentId, error);
})
.finally(() => {
detailFolderFetchRef.current.delete(currentId);
});
}
break;
}
const parentId = node.parentId ?? 'root';
if (!parentId || parentId === 'root') {
break;
}
currentId = parentId;
}
});
}, [orderedSelectedDocuments, folderNodes, ensureFolderData]);
const resolveFolderPath = useCallback(
(folderId) => {
if (!folderId || folderId === 'root') {
return [];
}
const segments = [];
const visited = new Set();
let currentId = folderId;
let guard = 0;
while (currentId && guard < 32 && !visited.has(currentId)) {
guard += 1;
visited.add(currentId);
if (currentId === 'root') {
break;
}
const node = folderNodes.get(currentId);
if (!node) {
segments.push({ id: currentId, name: '…' });
break;
}
segments.push({ id: node.id, name: node.name || 'Folder' });
const parentId = node.parentId ?? 'root';
if (!parentId || parentId === 'root') {
segments.push({ id: 'root', name: DEFAULT_FOLDER_NAME });
break;
}
currentId = parentId;
}
if (!segments.some((segment) => segment.id === 'root')) {
segments.push({ id: 'root', name: DEFAULT_FOLDER_NAME });
}
return segments.reverse();
},
[folderNodes],
);
const selectedPreviewEntry = useMemo(() => {
if (!selectedDocument) {
return null;
@@ -4598,7 +4804,9 @@ const AppLayout = () => {
onCorrespondentAdd: handleCorrespondentAdd,
onCorrespondentRemove: handleCorrespondentRemove,
resolveApiPath,
onFolderNavigate: selectFolder,
onClose: clearDocumentSelection,
resolveFolderPath,
};
const skeuoWorkspaceProps = useMemo(