simplify shit

This commit is contained in:
2025-12-07 01:31:34 +01:00
parent 47599aba4a
commit 632665b0ed
5 changed files with 147 additions and 466 deletions
@@ -37,8 +37,6 @@ type EnsureFolderData = (
options?: { includeDocuments?: boolean }, options?: { includeDocuments?: boolean },
) => Promise<FolderContents>; ) => Promise<FolderContents>;
type ApplySelectedFolder = (folderId: FolderId, contents?: FolderContents | null) => void;
type RemoveDocumentsFromCaches = (documentIds: DocumentId[]) => void; type RemoveDocumentsFromCaches = (documentIds: DocumentId[]) => void;
type CloseDocumentPreview = () => void; type CloseDocumentPreview = () => void;
@@ -86,7 +84,6 @@ interface UseDocumentMutationsArgs {
selectedFolder: FolderId; selectedFolder: FolderId;
setSelectedFolder: Dispatch<SetStateAction<FolderId>>; setSelectedFolder: Dispatch<SetStateAction<FolderId>>;
setDocuments: Dispatch<SetStateAction<Document[]>>; setDocuments: Dispatch<SetStateAction<Document[]>>;
setFolderContents: Dispatch<SetStateAction<Map<FolderId, FolderContents>>>;
setSearchResultIds: Dispatch<SetStateAction<DocumentId[] | null>>; setSearchResultIds: Dispatch<SetStateAction<DocumentId[] | null>>;
setSelectedEntries: Dispatch<SetStateAction<string[]>>; setSelectedEntries: Dispatch<SetStateAction<string[]>>;
setSelectionOrder: Dispatch<SetStateAction<string[]>>; setSelectionOrder: Dispatch<SetStateAction<string[]>>;
@@ -99,7 +96,6 @@ interface UseDocumentMutationsArgs {
notifyApiError: NotifyApiError; notifyApiError: NotifyApiError;
setStatusMessage: SetStatusMessage; setStatusMessage: SetStatusMessage;
mapDocumentCaches: MapDocumentCaches; mapDocumentCaches: MapDocumentCaches;
applySelectedFolder: ApplySelectedFolder;
folderNodes: Map<FolderId, FolderNode>; folderNodes: Map<FolderId, FolderNode>;
setFolderNodes: Dispatch<SetStateAction<Map<FolderId, FolderNode>>>; setFolderNodes: Dispatch<SetStateAction<Map<FolderId, FolderNode>>>;
removeDocumentsFromCaches: RemoveDocumentsFromCaches; removeDocumentsFromCaches: RemoveDocumentsFromCaches;
@@ -159,7 +155,6 @@ const useDocumentMutations = ({
selectedFolder, selectedFolder,
setSelectedFolder, setSelectedFolder,
setDocuments, setDocuments,
setFolderContents,
setSearchResultIds, setSearchResultIds,
setSelectedEntries, setSelectedEntries,
setSelectionOrder, setSelectionOrder,
@@ -172,7 +167,6 @@ const useDocumentMutations = ({
notifyApiError, notifyApiError,
setStatusMessage, setStatusMessage,
mapDocumentCaches, mapDocumentCaches,
applySelectedFolder,
folderNodes, folderNodes,
setFolderNodes, setFolderNodes,
removeDocumentsFromCaches, removeDocumentsFromCaches,
@@ -291,26 +285,8 @@ const useDocumentMutations = ({
return filtered.length === prev.length ? prev : filtered; return filtered.length === prev.length ? prev : filtered;
}); });
setDocuments((prev) => prev.filter((doc) => !uniqueIdSet.has(doc.id as DocumentId))); setDocuments((prev) => prev.filter((doc) => !uniqueIdSet.has(doc.id as DocumentId)));
setFolderContents((prev: Map<FolderId, FolderContents>) => { setDocuments((prev) => prev.filter((doc) => !uniqueIdSet.has(doc.id as DocumentId)));
if (!prev.size) { // setFolderContents removed as we don't hold full cache anymore
return prev;
}
let changed = false;
const next = new Map<FolderId, FolderContents>(prev);
movedDocs.forEach(({ id, sourceFolderId }) => {
const sourceKey = (sourceFolderId || 'root') as FolderId;
const entry = next.get(sourceKey);
if (!entry?.documents?.length) {
return;
}
const filteredDocs = entry.documents.filter((doc) => doc.id !== id);
if (filteredDocs.length !== entry.documents.length) {
changed = true;
next.set(sourceKey, { ...entry, documents: filteredDocs });
}
});
return changed ? next : prev;
});
setSelectedEntries((prev) => pruneRow(prev)); setSelectedEntries((prev) => pruneRow(prev));
setSelectionOrder((prev) => pruneRow(prev)); setSelectionOrder((prev) => pruneRow(prev));
@@ -350,7 +326,6 @@ const useDocumentMutations = ({
selectedFolder, selectedFolder,
setSearchResultIds, setSearchResultIds,
setDocuments, setDocuments,
setFolderContents,
setSelectedEntries, setSelectedEntries,
setSelectionOrder, setSelectionOrder,
selectionOrderRef, selectionOrderRef,
@@ -696,22 +671,17 @@ const useDocumentMutations = ({
}); });
} }
} }
return next;
}); });
setFolderContents((prev: Map<FolderId, FolderContents>) => { // setFolderContents removed
const next = new Map<FolderId, FolderContents>(prev);
next.delete(folderId);
return next;
});
if (selectedFolder === folderId) { if (selectedFolder === folderId) {
const node = folderNodes.get(folderId); const node = folderNodes.get(folderId);
const parentId = node?.parentId || 'root'; const parentId = node?.parentId || 'root';
setSelectedFolder(parentId); setSelectedFolder(parentId);
const parentContents = await ensureFolderData(parentId); // ensureFolderData(parentId) will be called by useDocumentsWorkspace effect when selectedFolder changes
applySelectedFolder(parentId, parentContents);
} else if (selectedFolder !== 'root') { } else if (selectedFolder !== 'root') {
// If deleted folder was not selected, just check if we need to refresh (maybe redundant)
await ensureFolderData(selectedFolder); await ensureFolderData(selectedFolder);
} }
@@ -734,9 +704,7 @@ const useDocumentMutations = ({
selectedFolder, selectedFolder,
folderNodes, folderNodes,
setSelectedFolder, setSelectedFolder,
applySelectedFolder,
setFolderNodes, setFolderNodes,
setFolderContents,
notifyApiError, notifyApiError,
setStatusMessage, setStatusMessage,
], ],
@@ -18,7 +18,7 @@ import useApiError from '../../hooks/useApiError';
import TagManager from '../../lib/assets/TagManager'; import TagManager from '../../lib/assets/TagManager';
import { useManagementModals } from '../../app/useManagementModals'; import { useManagementModals } from '../../app/useManagementModals';
import { useAppDispatch, useAppState } from '../../lib/store/appState'; import { useAppDispatch, useAppState } from '../../lib/store/appState';
import { fetchAsset } from '../../lib/api/apiClient'; import { fetchAsset, listFolderContents } from '../../lib/api/apiClient';
import { useApi } from '../../lib/context/ApiContext'; import { useApi } from '../../lib/context/ApiContext';
import { useWorkspaceSelection } from '../../app/useWorkspaceSelection'; import { useWorkspaceSelection } from '../../app/useWorkspaceSelection';
import { useEntryPointer as useEntryPointerCore } from '../features/selection/useEntryPointer'; import { useEntryPointer as useEntryPointerCore } from '../features/selection/useEntryPointer';
@@ -34,7 +34,12 @@ import {
createRootNode, createRootNode,
mergeAssetIntoDocument, mergeAssetIntoDocument,
} from '../../app/workspaceUtils'; } from '../../app/workspaceUtils';
import { createDocumentEntryKey, createFolderEntryKey } from '../../app/entryKey'; import {
createDocumentEntryKey,
createFolderEntryKey,
isFolderEntry,
isDocumentEntry
} from '../../app/entryKey';
import useDocumentsSearch from '../../app/useDocumentsSearch'; import useDocumentsSearch from '../../app/useDocumentsSearch';
import { useStatusToast, type ToastVariant } from '../../lib/context/StatusToastContext'; import { useStatusToast, type ToastVariant } from '../../lib/context/StatusToastContext';
import useAuthManager from './useAuthManager'; import useAuthManager from './useAuthManager';
@@ -281,27 +286,6 @@ const useDocumentsWorkspace = ({
configureSelectionEnvironment, configureSelectionEnvironment,
} = selection; } = selection;
const selectionHelpers = useMemo(
() => ({
setSelectedEntries,
setSelectionOrder,
selectionOrderRef,
selectionAnchorRef,
setFocusedDocumentId,
focusedDocumentId,
selectionInitializedRef,
}),
[
setSelectedEntries,
setSelectionOrder,
selectionOrderRef,
selectionAnchorRef,
setFocusedDocumentId,
focusedDocumentId,
selectionInitializedRef,
],
);
const [folderContents, setFolderContents] = useState<Map<FolderNodeId, FolderContentsEntry>>( const [folderContents, setFolderContents] = useState<Map<FolderNodeId, FolderContentsEntry>>(
() => new Map(), () => new Map(),
); );
@@ -339,27 +323,102 @@ const useDocumentsWorkspace = ({
setFolderNodes, setFolderNodes,
selectedFolder, selectedFolder,
setSelectedFolder, setSelectedFolder,
currentFolder,
setCurrentFolder,
currentSubfolders,
setCurrentSubfolders,
currentFolderName, currentFolderName,
folderOptions, folderOptions,
folderLabelMap, folderLabelMap,
applySelectedFolder,
ensureFolderData,
isInvalidFolderDrop, isInvalidFolderDrop,
} = useFolderTree({ } = useFolderTree({
initialSelectedFolder: routeFolderId || 'root', initialSelectedFolder: routeFolderId || 'root',
documentsSortFieldRef: activeSortFieldRef,
documentsSortDirectionRef: activeSortDirectionRef,
selectionHelpers,
setDocuments,
setFolderContents,
folderContentsRef,
foldersManager, foldersManager,
}); });
const [currentFolder, setCurrentFolder] = useState<FolderContentsEntry['folder'] | null>(null);
const [currentSubfolders, setCurrentSubfolders] = useState<FolderContentsEntry['subfolders']>([]);
const ensureFolderData = useCallback(
async (
folderId: FolderNodeId,
options: { includeDocuments?: boolean } = {}
) => {
try {
const path = folderId === 'root' ? 'root' : folderId;
const includeDocuments = options.includeDocuments ?? true;
const params: Record<string, unknown> = {
include_documents: includeDocuments,
sort: activeSortFieldRef.current,
dir: activeSortDirectionRef.current,
};
const data = await listFolderContents(path, params);
// Only update UI state if we are fetching for the currently selected folder
if (folderId === selectedFolder) {
// Update documents state if included
if (includeDocuments) {
setDocuments((data.documents || []) as Document[]);
}
setCurrentSubfolders((data.subfolders || []) as any[]);
setCurrentFolder(data.folder || null);
// Update selection state based on new documents
if (includeDocuments) {
const docs = (data.documents || []) as Document[];
const subfolders = (data.subfolders || []) as any[];
const availableDocKeys = docs
.map((doc) => createDocumentEntryKey(doc?.id as Identifier))
.filter(Boolean);
const availableDocKeySet = new Set(availableDocKeys);
const availableFolderKeys = new Set(
subfolders
.map((folder) => createFolderEntryKey(folder?.id as Identifier))
.filter(Boolean),
);
setSelectedEntries((previous) => {
const previousFolderKeys = previous
.filter(isFolderEntry)
.filter((key) => availableFolderKeys.has(key));
const previousDocKeys = previous.filter(isDocumentEntry);
const nextDocKeys = previousDocKeys.filter((key) => availableDocKeySet.has(key));
const mergedSelection = [...previousFolderKeys, ...nextDocKeys];
return mergedSelection;
});
}
}
return data; // Return data for consumers (e.g. useDocumentMutations)
} catch (error) {
notifyApiError(error, 'Failed to fetch folder contents');
throw error;
}
},
[
activeSortFieldRef,
activeSortDirectionRef,
setDocuments,
setCurrentFolder,
setSelectedEntries,
notifyApiError,
setCurrentSubfolders,
selectedFolder,
]
);
// Reactively fetch documents when selectedFolder changes
useEffect(() => {
if (selectedFolder) {
ensureFolderData(selectedFolder);
}
}, [selectedFolder, ensureFolderData]);
// Also re-fetch when sort changes
useEffect(() => {
if (selectedFolder && sortRefreshReadyRef?.current) {
ensureFolderData(selectedFolder);
}
}, [documentsSortField, documentsSortDirection, selectedFolder, ensureFolderData, sortRefreshReadyRef]);
const { const {
searchQuery, searchQuery,
setSearchQuery, setSearchQuery,
@@ -426,7 +485,7 @@ const useDocumentsWorkspace = ({
() => () =>
showingSearchResults showingSearchResults
? [] ? []
: currentSubfolders : (currentSubfolders || [])
.map((folder) => createFolderEntryKey(folder.id)) .map((folder) => createFolderEntryKey(folder.id))
.filter(Boolean), .filter(Boolean),
[showingSearchResults, currentSubfolders], [showingSearchResults, currentSubfolders],
@@ -577,9 +636,10 @@ const useDocumentsWorkspace = ({
); );
const refreshCurrentFolder = useCallback(async () => { const refreshCurrentFolder = useCallback(async () => {
const contents = await ensureFolderData(selectedFolder); if (selectedFolder) {
applySelectedFolder(selectedFolder, contents); await ensureFolderData(selectedFolder);
}, [selectedFolder, ensureFolderData, applySelectedFolder]); }
}, [selectedFolder, ensureFolderData]);
const { const {
handleBulkTagAddFromDetail, handleBulkTagAddFromDetail,
@@ -601,15 +661,16 @@ const useDocumentsWorkspace = ({
uploadQueue, uploadQueue,
clearUploadQueue, clearUploadQueue,
resetUploadsState, resetUploadsState,
handleFileSelection,
} = useDocumentUploads({ } = useDocumentUploads({
token, token,
selectedFolder, selectedFolder,
currentFolderName, currentFolderName,
ensureFolderData, ensureFolderData,
refreshCurrentFolder, refreshCurrentFolder,
shellRef,
notifyApiError, notifyApiError,
setStatusMessage, setStatusMessage,
shellRef,
}); });
const { const {
@@ -618,15 +679,15 @@ const useDocumentsWorkspace = ({
handleFolderDragStart, handleFolderDragStart,
handleFolderDragEnd, handleFolderDragEnd,
} = useDocumentDragHandlers({ } = useDocumentDragHandlers({
documentLookup,
setDraggedDocumentIds,
setDraggedFolderId,
documentsViewMode,
selectedEntries, selectedEntries,
selectedDocumentIds, selectedDocumentIds,
selectedFolderIds, selectedFolderIds,
applySelection, applySelection,
handleEntrySelection, handleEntrySelection,
documentLookup,
setDraggedDocumentIds,
setDraggedFolderId,
documentsViewMode,
}); });
useEffect(() => { useEffect(() => {
@@ -649,7 +710,6 @@ const useDocumentsWorkspace = ({
const resetWorkspaceState = useCallback(() => { const resetWorkspaceState = useCallback(() => {
const rootNode = createRootNode(); const rootNode = createRootNode();
setFolderNodes(new Map([[rootNode.id, rootNode]])); setFolderNodes(new Map([[rootNode.id, rootNode]]));
setFolderContents(new Map());
setSelectedFolder('root'); setSelectedFolder('root');
setCurrentFolder(null); setCurrentFolder(null);
setCurrentSubfolders([]); setCurrentSubfolders([]);
@@ -687,7 +747,7 @@ const useDocumentsWorkspace = ({
setSelectedEntries, setSelectedEntries,
setSelectionOrder, setSelectionOrder,
setFolderNodes, setFolderNodes,
setFolderContents, // setFolderContents,
setSelectedFolder, setSelectedFolder,
setCurrentFolder, setCurrentFolder,
setCurrentSubfolders, setCurrentSubfolders,
@@ -730,35 +790,11 @@ const useDocumentsWorkspace = ({
return filtered.length === prev.length ? prev : filtered; return filtered.length === prev.length ? prev : filtered;
}); });
setFolderContents((prev: Map<FolderNodeId, FolderContentsEntry>) => {
if (!prev.size) {
return prev;
}
let changed = false;
const next = new Map<FolderNodeId, FolderContentsEntry>();
prev.forEach((contents, key) => {
const docs = Array.isArray(contents?.documents) ? contents.documents : null;
if (!docs || docs.length === 0) {
next.set(key, contents);
return;
}
const filtered = docs.filter((doc) => !idSet.has(doc.id));
if (filtered.length !== docs.length) {
changed = true;
next.set(key, { ...contents, documents: filtered });
} else {
next.set(key, contents);
}
});
return changed ? next : prev;
});
removeDocumentsFromLookup(Array.from(idSet)); removeDocumentsFromLookup(Array.from(idSet));
}, },
[ [
setDocuments, setDocuments,
setSearchResultIds, setSearchResultIds,
setFolderContents,
removeDocumentsFromLookup, removeDocumentsFromLookup,
], ],
); );
@@ -780,7 +816,6 @@ const useDocumentsWorkspace = ({
selectedFolder, selectedFolder,
setSelectedFolder, setSelectedFolder,
setDocuments, setDocuments,
setFolderContents,
setSearchResultIds, setSearchResultIds,
setSelectedEntries, setSelectedEntries,
setSelectionOrder, setSelectionOrder,
@@ -793,7 +828,6 @@ const useDocumentsWorkspace = ({
notifyApiError, notifyApiError,
setStatusMessage, setStatusMessage,
mapDocumentCaches, mapDocumentCaches,
applySelectedFolder,
folderNodes, folderNodes,
setFolderNodes, setFolderNodes,
removeDocumentsFromCaches, removeDocumentsFromCaches,
@@ -822,14 +856,9 @@ const useDocumentsWorkspace = ({
setFolderNodes, setFolderNodes,
selectedFolder, selectedFolder,
setSelectedFolder, setSelectedFolder,
ensureFolderData,
applySelectedFolder,
notifyApiError, notifyApiError,
setStatusMessage, setStatusMessage,
setFolderContents,
setCurrentFolder, setCurrentFolder,
setSearchResultIds,
isFilterActive,
navigate, navigate,
handleFileDrop, handleFileDrop,
moveDocumentsToFolder, moveDocumentsToFolder,
@@ -883,8 +912,8 @@ const useDocumentsWorkspace = ({
return; return;
} }
const hasData = folderContents.has(targetParam); // Checking cache (folderContents) is removed, now we rely on selectedFolder effect to fetch.
if (targetParam !== selectedFolder || !hasData) { if (targetParam !== selectedFolder) {
selectFolder(targetParam, { immediate: true }); selectFolder(targetParam, { immediate: true });
} }
}, [ }, [
@@ -893,7 +922,6 @@ const useDocumentsWorkspace = ({
routeFolderId, routeFolderId,
routeDocumentId, routeDocumentId,
selectedFolder, selectedFolder,
folderContents,
isFilterActive, isFilterActive,
selectFolder, selectFolder,
]); ]);
@@ -1302,6 +1330,7 @@ const useDocumentsWorkspace = ({
tenants: tenantOptions, tenants: tenantOptions,
tenantOptions, tenantOptions,
handleTenantSelect, handleTenantSelect,
handleFileSelection,
}; };
// hook callers handle rendering / routing // hook callers handle rendering / routing
@@ -1,51 +1,11 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
import { createRootNode, DEFAULT_FOLDER_NAME, flattenFolderTree } from '../../../app/workspaceUtils'; import { createRootNode, DEFAULT_FOLDER_NAME, flattenFolderTree } from '../../../app/workspaceUtils';
import { listFolderContents } from '../../../lib/api/apiClient'; import type { FolderNodeId, FolderId } from '../../../types/identifiers';
import { import type { FolderNode } from '../../../types/documents';
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 FoldersManager from '../../FoldersManager'; 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 { interface UseFolderTreeOptions {
initialSelectedFolder?: FolderNodeId; 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; foldersManager?: FoldersManager;
} }
@@ -56,16 +16,10 @@ interface FolderOption {
const useFolderTree = ({ const useFolderTree = ({
initialSelectedFolder = 'root', initialSelectedFolder = 'root',
documentsSortFieldRef,
documentsSortDirectionRef,
selectionHelpers,
setDocuments,
setFolderContents,
folderContentsRef,
foldersManager, foldersManager,
}: UseFolderTreeOptions) => { }: UseFolderTreeOptions) => {
const [folderNodes, setFolderNodes] = useState<Map<FolderNodeId, FolderTreeNode>>(() => { const [folderNodes, setFolderNodes] = useState<Map<FolderNodeId, FolderNode>>(() => {
const rootNode = createRootNode() as FolderTreeNode; const rootNode = createRootNode() as FolderNode;
return new Map([[rootNode.id, rootNode]]); return new Map([[rootNode.id, rootNode]]);
}); });
@@ -110,7 +64,7 @@ const useFolderTree = ({
const root = next.get('root'); const root = next.get('root');
if (root) { if (root) {
next.set('root', { next.set('root', {
...(root as FolderTreeNode), ...(root as FolderNode),
children: rootChildren, children: rootChildren,
hasChildren: rootChildren.length > 0, hasChildren: rootChildren.length > 0,
loaded: true, loaded: true,
@@ -126,158 +80,6 @@ const useFolderTree = ({
}, [foldersManager]); }, [foldersManager]);
const [selectedFolder, setSelectedFolder] = useState<FolderNodeId>(initialSelectedFolder || 'root'); 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( const isInvalidFolderDrop = useCallback(
(sourceId: FolderNodeId | null, targetId: FolderNodeId | null) => { (sourceId: FolderNodeId | null, targetId: FolderNodeId | null) => {
@@ -298,7 +100,7 @@ const useFolderTree = ({
} }
const node = folderNodes.get(current); const node = folderNodes.get(current);
if (!node) break; if (!node) break;
current = node.parentId ?? 'root'; current = (node.parentId ?? 'root') as FolderNodeId;
} }
return false; return false;
}, },
@@ -306,18 +108,16 @@ const useFolderTree = ({
); );
const resetFolderTreeState = useCallback(() => { const resetFolderTreeState = useCallback(() => {
const rootNode = createRootNode() as FolderTreeNode; const rootNode = createRootNode() as FolderNode;
setFolderNodes(new Map<FolderNodeId, FolderTreeNode>([[rootNode.id, rootNode]])); setFolderNodes(new Map<FolderNodeId, FolderNode>([[rootNode.id, rootNode]]));
setFolderContents(new Map<FolderNodeId, FolderContentsEntry>());
setSelectedFolder('root'); setSelectedFolder('root');
setCurrentFolder(null); }, []);
setCurrentSubfolders([]);
}, [setFolderContents]);
const currentFolderName = useMemo(() => { const currentFolderName = useMemo(() => {
if (selectedFolder === 'root' || !currentFolder) return DEFAULT_FOLDER_NAME; if (selectedFolder === 'root') return DEFAULT_FOLDER_NAME;
return currentFolder.name; const node = folderNodes.get(selectedFolder);
}, [selectedFolder, currentFolder]); return node?.name || DEFAULT_FOLDER_NAME;
}, [selectedFolder, folderNodes]);
const folderOptions: FolderOption[] = useMemo(() => { const folderOptions: FolderOption[] = useMemo(() => {
const cache = new Map<FolderNodeId, string>(); const cache = new Map<FolderNodeId, string>();
@@ -376,15 +176,9 @@ const useFolderTree = ({
setFolderNodes, setFolderNodes,
selectedFolder, selectedFolder,
setSelectedFolder, setSelectedFolder,
currentFolder,
setCurrentFolder,
currentSubfolders,
setCurrentSubfolders,
currentFolderName, currentFolderName,
folderOptions, folderOptions,
folderLabelMap, folderLabelMap,
applySelectedFolder,
ensureFolderData,
isInvalidFolderDrop, isInvalidFolderDrop,
resetFolderTreeState, resetFolderTreeState,
}; };
@@ -8,31 +8,10 @@ import {
renameFolder as renameFolderRequest, renameFolder as renameFolderRequest,
} from '../../../lib/api/apiClient'; } from '../../../lib/api/apiClient';
import type { FolderId } from '../../../types/identifiers'; import type { FolderId } from '../../../types/identifiers';
import type { MessageOptions } from '../../../types/documents'; import type { MessageOptions, FolderNode } from '../../../types/documents';
type FolderKey = FolderId | 'root'; 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 { interface LoadFolderOptions {
preserveSearch?: boolean; preserveSearch?: boolean;
} }
@@ -55,16 +34,9 @@ interface UseFolderTreeActionsOptions {
setFolderNodes: (updater: (prev: Map<FolderKey, FolderNode>) => Map<FolderKey, FolderNode>) => void; setFolderNodes: (updater: (prev: Map<FolderKey, FolderNode>) => Map<FolderKey, FolderNode>) => void;
selectedFolder: FolderKey; selectedFolder: FolderKey;
setSelectedFolder: (folderId: FolderKey) => void; setSelectedFolder: (folderId: FolderKey) => void;
ensureFolderData: (folderId: FolderKey, options?: EnsureFolderOptions) => Promise<any>;
applySelectedFolder: (folderId: FolderKey, contents: any) => void;
notifyApiError: (error: unknown, message?: string) => void; notifyApiError: (error: unknown, message?: string) => void;
setStatusMessage: (message: string, level?: string) => void; setStatusMessage: (message: string, level?: string) => void;
setFolderContents: (
updater: (prev: Map<FolderKey, FolderContentsState>) => Map<FolderKey, FolderContentsState>,
) => void;
setCurrentFolder: (updater: (prev: any) => any) => void; setCurrentFolder: (updater: (prev: any) => any) => void;
setSearchResultIds: (value: any) => void;
isFilterActive: boolean;
navigate?: (path: string, options?: { replace?: boolean }) => void; navigate?: (path: string, options?: { replace?: boolean }) => void;
handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderKey) => Promise<void> | void; handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderKey) => Promise<void> | void;
moveDocumentsToFolder: (docIds: FolderId[], folderId: FolderKey) => Promise<void>; moveDocumentsToFolder: (docIds: FolderId[], folderId: FolderKey) => Promise<void>;
@@ -82,14 +54,9 @@ const useFolderTreeActions = ({
setFolderNodes, setFolderNodes,
selectedFolder, selectedFolder,
setSelectedFolder, setSelectedFolder,
ensureFolderData,
applySelectedFolder,
notifyApiError, notifyApiError,
setStatusMessage, setStatusMessage,
setFolderContents,
setCurrentFolder, setCurrentFolder,
setSearchResultIds,
isFilterActive,
navigate, navigate,
handleFileDrop, handleFileDrop,
moveDocumentsToFolder, moveDocumentsToFolder,
@@ -165,15 +132,7 @@ const useFolderTreeActions = ({
return next; return next;
}); });
const refreshTargets = new Set([previousParentKey, targetKey]);
await Promise.all(
Array.from(refreshTargets).map((key) =>
ensureFolderData(key),
),
);
if (selectedFolder === folderId) { if (selectedFolder === folderId) {
await ensureFolderData(folderId);
setSelectedFolder(folderId); setSelectedFolder(folderId);
} }
@@ -181,17 +140,9 @@ const useFolderTreeActions = ({
} catch (error) { } catch (error) {
const message = error.response?.data?.error || 'Failed to move folder.'; const message = error.response?.data?.error || 'Failed to move folder.';
notifyApiError(error, message); notifyApiError(error, message);
const refreshTargets = new Set([previousParentKey, targetKey]);
await Promise.all(
Array.from(refreshTargets).map((key) =>
ensureFolderData(key),
),
);
} }
}, },
[ [
ensureFolderData,
folderNodes, folderNodes,
notifyApiError, notifyApiError,
selectedFolder, selectedFolder,
@@ -202,35 +153,12 @@ const useFolderTreeActions = ({
); );
const loadFolder = useCallback( const loadFolder = useCallback(
async (folderId: FolderKey | null, { preserveSearch = false }: LoadFolderOptions = {}) => { async (folderId: FolderKey | null, { preserveSearch: _preserveSearch = false }: LoadFolderOptions = {}) => {
const targetId = folderId || 'root'; const targetId = folderId || 'root';
setSelectedFolder(targetId); setSelectedFolder(targetId);
try { // Data fetching is now reactive in the parent component based on selectedFolder
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.');
}
}, },
[ [setSelectedFolder],
applySelectedFolder,
ensureFolderData,
notifyApiError,
setSearchResultIds,
setSelectedFolder,
],
); );
const selectFolder = useCallback( const selectFolder = useCallback(
@@ -238,8 +166,7 @@ const useFolderTreeActions = ({
const targetId = folderId && folderId !== 'root' ? folderId : 'root'; const targetId = folderId && folderId !== 'root' ? folderId : 'root';
if (!navigate || immediate) { if (!navigate || immediate) {
await loadFolder(targetId, { preserveSearch: isFilterActive }); await loadFolder(targetId);
setSelectedFolder(targetId);
return; return;
} }
@@ -247,10 +174,8 @@ const useFolderTreeActions = ({
navigate(path, { replace }); navigate(path, { replace });
}, },
[ [
isFilterActive,
loadFolder, loadFolder,
navigate, navigate,
setSelectedFolder,
], ],
); );
@@ -277,19 +202,6 @@ const useFolderTreeActions = ({
return next; 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)); setCurrentFolder((prev) => (prev?.id === folderId ? { ...prev, name: trimmed } : prev));
setStatusMessage('Folder renamed.', 'success'); setStatusMessage('Folder renamed.', 'success');
return true; return true;
@@ -302,7 +214,6 @@ const useFolderTreeActions = ({
[ [
notifyApiError, notifyApiError,
setCurrentFolder, setCurrentFolder,
setFolderContents,
setFolderNodes, setFolderNodes,
setStatusMessage, setStatusMessage,
token, token,
@@ -361,10 +272,6 @@ const useFolderTreeActions = ({
return next; return next;
}); });
// Refresh the parent folder to ensure consistency
const refreshTarget = targetParentId || 'root';
await ensureFolderData(refreshTarget);
await selectFolder(folderData.id, { immediate: true }); await selectFolder(folderData.id, { immediate: true });
succeeded = true; succeeded = true;
return true; return true;
@@ -380,7 +287,6 @@ const useFolderTreeActions = ({
} }
}, },
[ [
ensureFolderData,
notifyApiError, notifyApiError,
selectFolder, selectFolder,
selectedFolder, selectedFolder,
@@ -407,16 +313,6 @@ const useFolderTreeActions = ({
} }
try { 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); await deleteFolder(folderId);
setFolderNodes((prev) => { setFolderNodes((prev) => {
@@ -438,20 +334,10 @@ const useFolderTreeActions = ({
return next; return next;
}); });
setFolderContents((prev) => {
const next = new Map(prev);
next.delete(folderId);
return next;
});
if (selectedFolder === folderId) { if (selectedFolder === folderId) {
const node = folderNodes.get(folderId); const node = folderNodes.get(folderId);
const parentId = node?.parentId || 'root'; const parentId = node?.parentId || 'root';
setSelectedFolder(parentId); setSelectedFolder(parentId);
const parentContents = await ensureFolderData(parentId);
applySelectedFolder(parentId, parentContents);
} else if (selectedFolder !== 'root') {
await ensureFolderData(selectedFolder);
} }
if (showMessage) { if (showMessage) {
@@ -469,12 +355,9 @@ const useFolderTreeActions = ({
}, },
[ [
token, token,
applySelectedFolder,
ensureFolderData,
folderNodes, folderNodes,
notifyApiError, notifyApiError,
selectedFolder, selectedFolder,
setFolderContents,
setFolderNodes, setFolderNodes,
setSelectedFolder, setSelectedFolder,
setStatusMessage, setStatusMessage,
+14 -7
View File
@@ -14,13 +14,6 @@ export interface DocumentCorrespondent {
count?: number; count?: number;
} }
export interface FolderContents {
folder: Folder | null;
documents: Document[];
subfolders: Folder[];
[key: string]: unknown;
}
/** /**
* A tag entity as returned by the API (includes usage_count). * A tag entity as returned by the API (includes usage_count).
* Use DocumentTag for the embedded version on documents. * Use DocumentTag for the embedded version on documents.
@@ -111,3 +104,17 @@ export type DocumentsListEntry = FolderEntry | DocumentEntry;
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
export type FolderEventHandler = (folder: Folder, event: MouseEvent<HTMLElement>) => void; export type FolderEventHandler = (folder: Folder, event: MouseEvent<HTMLElement>) => void;
/**
* Represents a folder node in the UI tree structure (flat map representation).
*/
export interface FolderNode {
id: Identifier | 'root';
name?: string;
parentId?: Identifier | 'root' | null;
children: (Identifier | 'root')[];
expanded?: boolean;
loaded?: boolean;
hasChildren?: boolean;
[key: string]: unknown;
}