simplify shit
This commit is contained in:
@@ -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,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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user