frontend: folders
This commit is contained in:
+209
-1
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user