simplify shit
This commit is contained in:
@@ -1,51 +1,11 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { createRootNode, DEFAULT_FOLDER_NAME, flattenFolderTree } from '../../../app/workspaceUtils';
|
||||
import { listFolderContents } from '../../../lib/api/apiClient';
|
||||
import {
|
||||
getEntryId,
|
||||
isDocumentEntry,
|
||||
isFolderEntry,
|
||||
createDocumentEntryKey,
|
||||
createFolderEntryKey,
|
||||
} from '../../../app/entryKey';
|
||||
import type { FolderNodeId, Identifier, FolderId } from '../../../types/identifiers';
|
||||
import type { Document, Folder, FolderContents } from '../../../types/documents';
|
||||
import type { FolderNodeId, FolderId } from '../../../types/identifiers';
|
||||
import type { FolderNode } from '../../../types/documents';
|
||||
import type FoldersManager from '../../FoldersManager';
|
||||
|
||||
interface FolderContentsEntry extends FolderContents {
|
||||
__includesDocuments?: boolean;
|
||||
__sortField?: string | null;
|
||||
__sortDirection?: string | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface FolderTreeNode extends Folder {
|
||||
id: FolderNodeId;
|
||||
children: FolderNodeId[];
|
||||
expanded?: boolean;
|
||||
loaded?: boolean;
|
||||
hasChildren?: boolean;
|
||||
parentId?: FolderNodeId;
|
||||
}
|
||||
|
||||
interface SelectionHelpers {
|
||||
focusedDocumentId: Identifier | null;
|
||||
setFocusedDocumentId: Dispatch<SetStateAction<Identifier | null>>;
|
||||
setSelectedEntries: Dispatch<SetStateAction<string[]>>;
|
||||
setSelectionOrder: Dispatch<SetStateAction<string[]>>;
|
||||
selectionOrderRef: MutableRefObject<string[] | null>;
|
||||
selectionAnchorRef: MutableRefObject<string | null>;
|
||||
}
|
||||
|
||||
interface UseFolderTreeOptions {
|
||||
initialSelectedFolder?: FolderNodeId;
|
||||
documentsSortFieldRef: MutableRefObject<string>;
|
||||
documentsSortDirectionRef: MutableRefObject<string>;
|
||||
selectionHelpers: SelectionHelpers;
|
||||
setDocuments: Dispatch<SetStateAction<Document[]>>;
|
||||
setFolderContents: Dispatch<SetStateAction<Map<FolderNodeId, FolderContentsEntry>>>;
|
||||
folderContentsRef: MutableRefObject<Map<FolderNodeId, FolderContentsEntry>>;
|
||||
foldersManager?: FoldersManager;
|
||||
}
|
||||
|
||||
@@ -56,16 +16,10 @@ interface FolderOption {
|
||||
|
||||
const useFolderTree = ({
|
||||
initialSelectedFolder = 'root',
|
||||
documentsSortFieldRef,
|
||||
documentsSortDirectionRef,
|
||||
selectionHelpers,
|
||||
setDocuments,
|
||||
setFolderContents,
|
||||
folderContentsRef,
|
||||
foldersManager,
|
||||
}: UseFolderTreeOptions) => {
|
||||
const [folderNodes, setFolderNodes] = useState<Map<FolderNodeId, FolderTreeNode>>(() => {
|
||||
const rootNode = createRootNode() as FolderTreeNode;
|
||||
const [folderNodes, setFolderNodes] = useState<Map<FolderNodeId, FolderNode>>(() => {
|
||||
const rootNode = createRootNode() as FolderNode;
|
||||
return new Map([[rootNode.id, rootNode]]);
|
||||
});
|
||||
|
||||
@@ -110,7 +64,7 @@ const useFolderTree = ({
|
||||
const root = next.get('root');
|
||||
if (root) {
|
||||
next.set('root', {
|
||||
...(root as FolderTreeNode),
|
||||
...(root as FolderNode),
|
||||
children: rootChildren,
|
||||
hasChildren: rootChildren.length > 0,
|
||||
loaded: true,
|
||||
@@ -126,158 +80,6 @@ const useFolderTree = ({
|
||||
}, [foldersManager]);
|
||||
|
||||
const [selectedFolder, setSelectedFolder] = useState<FolderNodeId>(initialSelectedFolder || 'root');
|
||||
const [currentFolder, setCurrentFolder] = useState<Folder | null>(null);
|
||||
const [currentSubfolders, setCurrentSubfolders] = useState<Folder[]>([]);
|
||||
|
||||
const {
|
||||
focusedDocumentId,
|
||||
setFocusedDocumentId,
|
||||
setSelectedEntries,
|
||||
setSelectionOrder,
|
||||
selectionOrderRef,
|
||||
selectionAnchorRef,
|
||||
} = selectionHelpers;
|
||||
|
||||
const focusedDocumentIdRef = useRef(focusedDocumentId);
|
||||
useEffect(() => {
|
||||
focusedDocumentIdRef.current = focusedDocumentId;
|
||||
}, [focusedDocumentId]);
|
||||
|
||||
const applySelectedFolder = useCallback(
|
||||
(folderId: FolderNodeId, contents?: FolderContentsEntry | null) => {
|
||||
const subfolders = Array.isArray(contents?.subfolders) ? contents.subfolders : [];
|
||||
const docs = Array.isArray(contents?.documents) ? contents.documents : [];
|
||||
const folderInfo = contents?.folder ?? null;
|
||||
|
||||
setCurrentSubfolders(subfolders);
|
||||
setDocuments(docs);
|
||||
setCurrentFolder(folderInfo);
|
||||
|
||||
const availableDocKeys = docs
|
||||
.map((doc) => createDocumentEntryKey((doc as Document)?.id as Identifier))
|
||||
.filter(Boolean);
|
||||
const availableDocKeySet = new Set(availableDocKeys);
|
||||
const availableFolderKeys = new Set(
|
||||
subfolders
|
||||
.map((folder) => createFolderEntryKey(folder?.id as Identifier))
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
let nextDocKeys: string[] = [];
|
||||
let mergedSelection: string[] = [];
|
||||
|
||||
setSelectedEntries((previous) => {
|
||||
const previousFolderKeys = previous
|
||||
.filter(isFolderEntry)
|
||||
.filter((key) => availableFolderKeys.has(key));
|
||||
const previousDocKeys = previous.filter(isDocumentEntry);
|
||||
nextDocKeys = previousDocKeys.filter((key) => availableDocKeySet.has(key));
|
||||
mergedSelection = [...previousFolderKeys, ...nextDocKeys];
|
||||
return mergedSelection;
|
||||
});
|
||||
|
||||
const nextFocus = (() => {
|
||||
const currentFocusedId = focusedDocumentIdRef.current;
|
||||
if (currentFocusedId) {
|
||||
const currentFocusedKey = createDocumentEntryKey(currentFocusedId);
|
||||
if (currentFocusedKey && availableDocKeySet.has(currentFocusedKey)) {
|
||||
return currentFocusedId;
|
||||
}
|
||||
}
|
||||
if (nextDocKeys.length) {
|
||||
const lastDocKey = nextDocKeys[nextDocKeys.length - 1];
|
||||
return getEntryId(lastDocKey) || null;
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
|
||||
setFocusedDocumentId(nextFocus);
|
||||
const nextAnchor = mergedSelection.length ? mergedSelection[mergedSelection.length - 1] : null;
|
||||
selectionAnchorRef.current = nextAnchor;
|
||||
selectionOrderRef.current = mergedSelection;
|
||||
setSelectionOrder(mergedSelection);
|
||||
},
|
||||
[
|
||||
selectionAnchorRef,
|
||||
selectionOrderRef,
|
||||
setDocuments,
|
||||
setFocusedDocumentId,
|
||||
setSelectedEntries,
|
||||
setSelectionOrder,
|
||||
],
|
||||
);
|
||||
|
||||
const ensureFolderData = useCallback(
|
||||
async (
|
||||
folderId: FolderNodeId,
|
||||
{
|
||||
includeDocuments = true,
|
||||
sortField = documentsSortFieldRef.current,
|
||||
sortDirection = documentsSortDirectionRef.current,
|
||||
}: {
|
||||
includeDocuments?: boolean;
|
||||
sortField?: string;
|
||||
sortDirection?: string;
|
||||
} = {},
|
||||
): Promise<FolderContentsEntry> => {
|
||||
const cached = folderContentsRef.current.get(folderId);
|
||||
const cachedSortField = cached?.__sortField || documentsSortFieldRef.current;
|
||||
const cachedSortDirection = cached?.__sortDirection || documentsSortDirectionRef.current;
|
||||
const cachedSortMatches = cachedSortField === sortField && cachedSortDirection === sortDirection;
|
||||
|
||||
if (cached) {
|
||||
const includesDocuments = Boolean(cached.__includesDocuments);
|
||||
if (!includeDocuments || (includesDocuments && cachedSortMatches)) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
// We use listFolderContents purely for Documents now, assuming Tree is authoritative for subfolders.
|
||||
const path = folderId === 'root' ? 'root' : folderId;
|
||||
const params: Record<string, unknown> = {};
|
||||
if (!includeDocuments) {
|
||||
params.include_documents = false;
|
||||
} else {
|
||||
params.sort = sortField;
|
||||
params.dir = sortDirection;
|
||||
}
|
||||
|
||||
const data = await listFolderContents(path, params);
|
||||
|
||||
const enriched = {
|
||||
...data,
|
||||
documents: (data.documents || []) as Document[],
|
||||
__includesDocuments: includeDocuments,
|
||||
__sortField: includeDocuments ? sortField : cachedSortField,
|
||||
__sortDirection: includeDocuments ? sortDirection : cachedSortDirection,
|
||||
};
|
||||
|
||||
setFolderContents((prev: Map<FolderNodeId, FolderContentsEntry>) => {
|
||||
const next = new Map<FolderNodeId, FolderContentsEntry>(prev);
|
||||
if (includeDocuments) {
|
||||
next.set(folderId, enriched);
|
||||
} else {
|
||||
const existing = next.get(folderId);
|
||||
next.set(folderId, {
|
||||
...enriched,
|
||||
documents: existing?.__includesDocuments ? existing.documents : (enriched.documents as Document[]),
|
||||
__includesDocuments: existing?.__includesDocuments || false,
|
||||
});
|
||||
}
|
||||
folderContentsRef.current = next;
|
||||
return next;
|
||||
});
|
||||
|
||||
return enriched;
|
||||
},
|
||||
[
|
||||
documentsSortDirectionRef,
|
||||
documentsSortFieldRef,
|
||||
setFolderContents,
|
||||
folderContentsRef,
|
||||
// Removed foldersManager dependency as we don't call it here anymore
|
||||
],
|
||||
);
|
||||
|
||||
const isInvalidFolderDrop = useCallback(
|
||||
(sourceId: FolderNodeId | null, targetId: FolderNodeId | null) => {
|
||||
@@ -298,7 +100,7 @@ const useFolderTree = ({
|
||||
}
|
||||
const node = folderNodes.get(current);
|
||||
if (!node) break;
|
||||
current = node.parentId ?? 'root';
|
||||
current = (node.parentId ?? 'root') as FolderNodeId;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@@ -306,18 +108,16 @@ const useFolderTree = ({
|
||||
);
|
||||
|
||||
const resetFolderTreeState = useCallback(() => {
|
||||
const rootNode = createRootNode() as FolderTreeNode;
|
||||
setFolderNodes(new Map<FolderNodeId, FolderTreeNode>([[rootNode.id, rootNode]]));
|
||||
setFolderContents(new Map<FolderNodeId, FolderContentsEntry>());
|
||||
const rootNode = createRootNode() as FolderNode;
|
||||
setFolderNodes(new Map<FolderNodeId, FolderNode>([[rootNode.id, rootNode]]));
|
||||
setSelectedFolder('root');
|
||||
setCurrentFolder(null);
|
||||
setCurrentSubfolders([]);
|
||||
}, [setFolderContents]);
|
||||
}, []);
|
||||
|
||||
const currentFolderName = useMemo(() => {
|
||||
if (selectedFolder === 'root' || !currentFolder) return DEFAULT_FOLDER_NAME;
|
||||
return currentFolder.name;
|
||||
}, [selectedFolder, currentFolder]);
|
||||
if (selectedFolder === 'root') return DEFAULT_FOLDER_NAME;
|
||||
const node = folderNodes.get(selectedFolder);
|
||||
return node?.name || DEFAULT_FOLDER_NAME;
|
||||
}, [selectedFolder, folderNodes]);
|
||||
|
||||
const folderOptions: FolderOption[] = useMemo(() => {
|
||||
const cache = new Map<FolderNodeId, string>();
|
||||
@@ -376,15 +176,9 @@ const useFolderTree = ({
|
||||
setFolderNodes,
|
||||
selectedFolder,
|
||||
setSelectedFolder,
|
||||
currentFolder,
|
||||
setCurrentFolder,
|
||||
currentSubfolders,
|
||||
setCurrentSubfolders,
|
||||
currentFolderName,
|
||||
folderOptions,
|
||||
folderLabelMap,
|
||||
applySelectedFolder,
|
||||
ensureFolderData,
|
||||
isInvalidFolderDrop,
|
||||
resetFolderTreeState,
|
||||
};
|
||||
|
||||
@@ -8,31 +8,10 @@ import {
|
||||
renameFolder as renameFolderRequest,
|
||||
} from '../../../lib/api/apiClient';
|
||||
import type { FolderId } from '../../../types/identifiers';
|
||||
import type { MessageOptions } from '../../../types/documents';
|
||||
import type { MessageOptions, FolderNode } from '../../../types/documents';
|
||||
|
||||
type FolderKey = FolderId | 'root';
|
||||
|
||||
interface FolderNode {
|
||||
id: FolderKey;
|
||||
name?: string;
|
||||
parentId?: FolderKey | null;
|
||||
children: FolderId[];
|
||||
expanded?: boolean;
|
||||
loaded?: boolean;
|
||||
hasChildren?: boolean;
|
||||
}
|
||||
|
||||
interface FolderContentsState {
|
||||
folder?: { id: FolderKey; name?: string };
|
||||
subfolders?: any[];
|
||||
documents?: any[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface EnsureFolderOptions {
|
||||
includeDocuments?: boolean;
|
||||
}
|
||||
|
||||
interface LoadFolderOptions {
|
||||
preserveSearch?: boolean;
|
||||
}
|
||||
@@ -55,16 +34,9 @@ interface UseFolderTreeActionsOptions {
|
||||
setFolderNodes: (updater: (prev: Map<FolderKey, FolderNode>) => Map<FolderKey, FolderNode>) => void;
|
||||
selectedFolder: FolderKey;
|
||||
setSelectedFolder: (folderId: FolderKey) => void;
|
||||
ensureFolderData: (folderId: FolderKey, options?: EnsureFolderOptions) => Promise<any>;
|
||||
applySelectedFolder: (folderId: FolderKey, contents: any) => void;
|
||||
notifyApiError: (error: unknown, message?: string) => void;
|
||||
setStatusMessage: (message: string, level?: string) => void;
|
||||
setFolderContents: (
|
||||
updater: (prev: Map<FolderKey, FolderContentsState>) => Map<FolderKey, FolderContentsState>,
|
||||
) => void;
|
||||
setCurrentFolder: (updater: (prev: any) => any) => void;
|
||||
setSearchResultIds: (value: any) => void;
|
||||
isFilterActive: boolean;
|
||||
navigate?: (path: string, options?: { replace?: boolean }) => void;
|
||||
handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderKey) => Promise<void> | void;
|
||||
moveDocumentsToFolder: (docIds: FolderId[], folderId: FolderKey) => Promise<void>;
|
||||
@@ -82,14 +54,9 @@ const useFolderTreeActions = ({
|
||||
setFolderNodes,
|
||||
selectedFolder,
|
||||
setSelectedFolder,
|
||||
ensureFolderData,
|
||||
applySelectedFolder,
|
||||
notifyApiError,
|
||||
setStatusMessage,
|
||||
setFolderContents,
|
||||
setCurrentFolder,
|
||||
setSearchResultIds,
|
||||
isFilterActive,
|
||||
navigate,
|
||||
handleFileDrop,
|
||||
moveDocumentsToFolder,
|
||||
@@ -165,15 +132,7 @@ const useFolderTreeActions = ({
|
||||
return next;
|
||||
});
|
||||
|
||||
const refreshTargets = new Set([previousParentKey, targetKey]);
|
||||
await Promise.all(
|
||||
Array.from(refreshTargets).map((key) =>
|
||||
ensureFolderData(key),
|
||||
),
|
||||
);
|
||||
|
||||
if (selectedFolder === folderId) {
|
||||
await ensureFolderData(folderId);
|
||||
setSelectedFolder(folderId);
|
||||
}
|
||||
|
||||
@@ -181,17 +140,9 @@ const useFolderTreeActions = ({
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.error || 'Failed to move folder.';
|
||||
notifyApiError(error, message);
|
||||
|
||||
const refreshTargets = new Set([previousParentKey, targetKey]);
|
||||
await Promise.all(
|
||||
Array.from(refreshTargets).map((key) =>
|
||||
ensureFolderData(key),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
ensureFolderData,
|
||||
folderNodes,
|
||||
notifyApiError,
|
||||
selectedFolder,
|
||||
@@ -202,35 +153,12 @@ const useFolderTreeActions = ({
|
||||
);
|
||||
|
||||
const loadFolder = useCallback(
|
||||
async (folderId: FolderKey | null, { preserveSearch = false }: LoadFolderOptions = {}) => {
|
||||
async (folderId: FolderKey | null, { preserveSearch: _preserveSearch = false }: LoadFolderOptions = {}) => {
|
||||
const targetId = folderId || 'root';
|
||||
setSelectedFolder(targetId);
|
||||
try {
|
||||
const contents = await ensureFolderData(targetId);
|
||||
if (targetId !== 'root') {
|
||||
try {
|
||||
await ensureFolderData('root', {
|
||||
includeDocuments: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to refresh root folder tree', error);
|
||||
}
|
||||
}
|
||||
applySelectedFolder(targetId, contents);
|
||||
if (!preserveSearch) {
|
||||
setSearchResultIds(null);
|
||||
}
|
||||
} catch (error) {
|
||||
notifyApiError(error, 'Failed to load folder contents.');
|
||||
}
|
||||
// Data fetching is now reactive in the parent component based on selectedFolder
|
||||
},
|
||||
[
|
||||
applySelectedFolder,
|
||||
ensureFolderData,
|
||||
notifyApiError,
|
||||
setSearchResultIds,
|
||||
setSelectedFolder,
|
||||
],
|
||||
[setSelectedFolder],
|
||||
);
|
||||
|
||||
const selectFolder = useCallback(
|
||||
@@ -238,8 +166,7 @@ const useFolderTreeActions = ({
|
||||
const targetId = folderId && folderId !== 'root' ? folderId : 'root';
|
||||
|
||||
if (!navigate || immediate) {
|
||||
await loadFolder(targetId, { preserveSearch: isFilterActive });
|
||||
setSelectedFolder(targetId);
|
||||
await loadFolder(targetId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -247,10 +174,8 @@ const useFolderTreeActions = ({
|
||||
navigate(path, { replace });
|
||||
},
|
||||
[
|
||||
isFilterActive,
|
||||
loadFolder,
|
||||
navigate,
|
||||
setSelectedFolder,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -277,19 +202,6 @@ const useFolderTreeActions = ({
|
||||
return next;
|
||||
});
|
||||
|
||||
setFolderContents((prev) => {
|
||||
if (!prev.has(folderId)) {
|
||||
return prev;
|
||||
}
|
||||
const next = new Map(prev);
|
||||
const existing = next.get(folderId) || {};
|
||||
const folderInfo = existing.folder
|
||||
? { ...existing.folder, name: trimmed }
|
||||
: { id: folderId, name: trimmed };
|
||||
next.set(folderId, { ...existing, folder: folderInfo });
|
||||
return next;
|
||||
});
|
||||
|
||||
setCurrentFolder((prev) => (prev?.id === folderId ? { ...prev, name: trimmed } : prev));
|
||||
setStatusMessage('Folder renamed.', 'success');
|
||||
return true;
|
||||
@@ -302,7 +214,6 @@ const useFolderTreeActions = ({
|
||||
[
|
||||
notifyApiError,
|
||||
setCurrentFolder,
|
||||
setFolderContents,
|
||||
setFolderNodes,
|
||||
setStatusMessage,
|
||||
token,
|
||||
@@ -361,10 +272,6 @@ const useFolderTreeActions = ({
|
||||
return next;
|
||||
});
|
||||
|
||||
// Refresh the parent folder to ensure consistency
|
||||
const refreshTarget = targetParentId || 'root';
|
||||
await ensureFolderData(refreshTarget);
|
||||
|
||||
await selectFolder(folderData.id, { immediate: true });
|
||||
succeeded = true;
|
||||
return true;
|
||||
@@ -380,7 +287,6 @@ const useFolderTreeActions = ({
|
||||
}
|
||||
},
|
||||
[
|
||||
ensureFolderData,
|
||||
notifyApiError,
|
||||
selectFolder,
|
||||
selectedFolder,
|
||||
@@ -407,16 +313,6 @@ const useFolderTreeActions = ({
|
||||
}
|
||||
|
||||
try {
|
||||
const contents = await ensureFolderData(folderId);
|
||||
const hasChildren = (contents.subfolders || []).length > 0;
|
||||
const hasDocs = (contents.documents || []).length > 0;
|
||||
if (hasChildren || hasDocs) {
|
||||
if (showMessage) {
|
||||
setStatusMessage('Folder must be empty before it can be deleted.', 'error');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
await deleteFolder(folderId);
|
||||
|
||||
setFolderNodes((prev) => {
|
||||
@@ -438,20 +334,10 @@ const useFolderTreeActions = ({
|
||||
return next;
|
||||
});
|
||||
|
||||
setFolderContents((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.delete(folderId);
|
||||
return next;
|
||||
});
|
||||
|
||||
if (selectedFolder === folderId) {
|
||||
const node = folderNodes.get(folderId);
|
||||
const parentId = node?.parentId || 'root';
|
||||
setSelectedFolder(parentId);
|
||||
const parentContents = await ensureFolderData(parentId);
|
||||
applySelectedFolder(parentId, parentContents);
|
||||
} else if (selectedFolder !== 'root') {
|
||||
await ensureFolderData(selectedFolder);
|
||||
}
|
||||
|
||||
if (showMessage) {
|
||||
@@ -469,12 +355,9 @@ const useFolderTreeActions = ({
|
||||
},
|
||||
[
|
||||
token,
|
||||
applySelectedFolder,
|
||||
ensureFolderData,
|
||||
folderNodes,
|
||||
notifyApiError,
|
||||
selectedFolder,
|
||||
setFolderContents,
|
||||
setFolderNodes,
|
||||
setSelectedFolder,
|
||||
setStatusMessage,
|
||||
|
||||
Reference in New Issue
Block a user