feat: Add document move and tag mutations, refine document upload logic

This commit is contained in:
2025-12-08 16:27:13 +01:00
parent 5632d2252a
commit 3a0ef7867e
8 changed files with 709 additions and 695 deletions
@@ -214,7 +214,7 @@ const useDocumentsWorkspace = ({
}
const tagManager = tagManagerRef.current;
const selection = useWorkspaceSelection();
const selectionState = useWorkspaceSelection();
const {
selectedEntries,
@@ -234,7 +234,7 @@ const useDocumentsWorkspace = ({
clearSelection,
promoteSelectionOrder: promoteSelectionOrderRaw,
configureSelectionEnvironment,
} = selection;
} = selectionState;
const {
documents,
@@ -247,18 +247,22 @@ const useDocumentsWorkspace = ({
fetchDocumentById,
});
const foldersManagerRef = useRef<FoldersManager | null>(null);
if (!foldersManagerRef.current) {
foldersManagerRef.current = new FoldersManager();
}
const foldersManager = foldersManagerRef.current;
const documentLookup = useSyncExternalStore(
(onStoreChange) => documentsManager.subscribe(onStoreChange),
() => documentsManager.getSnapshot(),
() => documentsManager.getSnapshot(),
);
const foldersManagerRef = useRef<FoldersManager | null>(null);
if (!foldersManagerRef.current) {
foldersManagerRef.current = new FoldersManager();
}
const foldersManager = foldersManagerRef.current;
const folderStateRaw = useFolderTree({
initialSelectedFolder: routeFolderId || 'root',
foldersManager,
});
const {
folderNodes,
setFolderNodes,
@@ -266,21 +270,75 @@ const useDocumentsWorkspace = ({
setSelectedFolder,
currentFolderName,
folderOptions,
folderLabelMap,
isInvalidFolderDrop,
} = useFolderTree({
initialSelectedFolder: routeFolderId || 'root',
foldersManager,
});
} = folderStateRaw;
const folderState = {
...folderStateRaw,
setCreatingFolder,
};
const [currentSubfolders, setCurrentSubfolders] = useState<Array<{ id?: FolderNodeId; name?: string | null;[key: string]: unknown }>>([]);
const ensureFolderData = useCallback(
const reconcileSelectionWithFolderData = useCallback(
(currentSelection: string[], docs: Document[], subfolders: 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),
);
const previousFolderKeys = currentSelection
.filter(isFolderEntry)
.filter((key) => availableFolderKeys.has(key));
const previousDocKeys = currentSelection.filter(isDocumentEntry);
const nextDocKeys = previousDocKeys.filter((key) => availableDocKeySet.has(key));
return [...previousFolderKeys, ...nextDocKeys];
},
[],
);
const selectedFolderRef = useRef<FolderNodeId>(selectedFolder);
useEffect(() => {
selectedFolderRef.current = selectedFolder;
}, [selectedFolder]);
const updateViewState = useCallback(
(folderId: FolderNodeId, data: any, includeDocuments: boolean) => {
// Guard against race conditions: only update if the folder is still selected
if (folderId === selectedFolderRef.current) {
if (includeDocuments) {
setDocuments((data.documents || []) as Document[]);
}
setCurrentSubfolders((data.subfolders || []) as any[]);
if (includeDocuments) {
setSelectedEntries((prev) => reconcileSelectionWithFolderData(
prev,
(data.documents || []) as Document[],
(data.subfolders || []) as any[]
));
}
}
},
[
setDocuments,
setSelectedEntries,
setCurrentSubfolders,
reconcileSelectionWithFolderData,
selectedFolderRef,
]
);
const fetchFolderData = useCallback(
async (
folderId: FolderNodeId,
options: { includeDocuments?: boolean } = {}
) => {
const path = folderId === 'root' ? 'root' : folderId;
const includeDocuments = options.includeDocuments ?? true;
const params: Record<string, unknown> = {
@@ -290,61 +348,25 @@ const useDocumentsWorkspace = ({
};
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[]);
// 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)
return { data, includeDocuments };
},
[
activeSortFieldRef,
activeSortDirectionRef,
setDocuments,
setSelectedEntries,
setCurrentSubfolders,
selectedFolder,
]
);
useEffect(() => {
if (selectedFolder) {
ensureFolderData(selectedFolder).catch((error) => {
notifyApiError(error, 'Failed to fetch folder contents');
});
fetchFolderData(selectedFolder)
.then(({ data, includeDocuments }) => {
updateViewState(selectedFolder, data, includeDocuments);
})
.catch((error) => {
notifyApiError(error, 'Failed to fetch folder contents');
});
}
}, [selectedFolder, documentsSortField, documentsSortDirection, ensureFolderData, notifyApiError]);
}, [selectedFolder, documentsSortField, documentsSortDirection, fetchFolderData, updateViewState, notifyApiError]);
const {
searchQuery,
@@ -472,6 +494,12 @@ const useDocumentsWorkspace = ({
selectionInitializedRef,
});
const tagsStateRaw = useTags({
tenantIdRef,
tagManager,
setActiveTagFilters,
mapDocumentCaches,
});
const {
tags,
refreshTags,
@@ -479,13 +507,9 @@ const useDocumentsWorkspace = ({
handleTagUpdate,
handleTagDelete,
setTags,
} = useTags({
tenantIdRef,
tagManager,
setActiveTagFilters,
mapDocumentCaches,
});
} = tagsStateRaw;
// tagLookupById is derived locally
useEffect(() => {
tenantIdRef.current = currentTenantId;
}, [currentTenantId, tenantIdRef]);
@@ -500,6 +524,16 @@ const useDocumentsWorkspace = ({
return map;
}, [tags]);
const tagsState = {
...tagsStateRaw,
tagLookupById, // Add derived lookup
tagManager,
};
const correspondentsStateRaw = useCorrespondents({
tenantIdRef,
mapDocumentCaches,
});
const {
correspondents,
refreshCorrespondents,
@@ -507,10 +541,7 @@ const useDocumentsWorkspace = ({
handleCorrespondentUpdate,
handleCorrespondentDelete,
setCorrespondents,
} = useCorrespondents({
tenantIdRef,
mapDocumentCaches,
});
} = correspondentsStateRaw;
const {
correspondentLookupByName,
@@ -549,9 +580,10 @@ const useDocumentsWorkspace = ({
const refreshCurrentFolder = useCallback(async () => {
if (selectedFolder) {
await ensureFolderData(selectedFolder);
const { data, includeDocuments } = await fetchFolderData(selectedFolder);
updateViewState(selectedFolder, data, includeDocuments);
}
}, [selectedFolder, ensureFolderData]);
}, [selectedFolder, fetchFolderData, updateViewState]);
const {
handleBulkTagAddFromDetail,
@@ -575,7 +607,6 @@ const useDocumentsWorkspace = ({
} = useDocumentUploads({
selectedFolder,
currentFolderName,
ensureFolderData,
refreshCurrentFolder,
shellRef,
});
@@ -686,6 +717,22 @@ const useDocumentsWorkspace = ({
],
);
const documentsState = {
documentLookup,
setDocuments,
setSearchResultIds,
removeDocumentsFromCaches,
updateDocumentCaches,
mapDocumentCaches,
extractDocumentFromResponse,
ingestDocuments: (docs: unknown[]) => documentsManager.ingest(docs),
};
const actionsState = {
refreshCurrentFolder,
closeDocumentPreview,
};
const {
moveDocumentsToFolder,
handleThumbnailRegeneration,
@@ -696,37 +743,21 @@ const useDocumentsWorkspace = ({
handleDocumentIssuedUpdate,
handleDocumentTagDetach,
} = useDocumentMutations({
documentLookup,
folderLabelMap,
ensureFolderData,
selectedFolder,
setSelectedFolder,
setDocuments,
setSearchResultIds,
setSelectedEntries,
setSelectionOrder,
selectionOrderRef,
selectionAnchorRef,
setFocusedDocumentId,
focusedDocumentId,
setFocusedEntryKey,
focusedEntryKey,
mapDocumentCaches,
folderNodes,
setFolderNodes,
removeDocumentsFromCaches,
closeDocumentPreview,
documentsState,
folderState,
selectionState,
tagsState,
actions: actionsState,
previewDocumentId,
refreshCurrentFolder,
updateDocumentCaches,
tagLookupById,
tags,
refreshTags,
tagManager,
extractDocumentFromResponse,
ingestDocuments: (docs) => documentsManager.ingest(docs),
});
const dragState = {
draggedDocumentIds,
draggedFolderId,
setDraggedDocumentIds,
setDraggedFolderId,
};
const {
loadFolder,
selectFolder,
@@ -735,18 +766,15 @@ const useDocumentsWorkspace = ({
handleFolderDelete,
folderClickHandlers,
} = useFolderTreeActions({
folderNodes,
setFolderNodes,
selectedFolder,
setSelectedFolder,
handleFileDrop,
moveDocumentsToFolder,
draggedDocumentIds,
draggedFolderId,
setDraggedDocumentIds,
setDraggedFolderId,
isInvalidFolderDrop,
setCreatingFolder,
folderState,
dragState,
actions: {
handleFileDrop,
moveDocumentsToFolder,
},
utils: {
isInvalidFolderDrop,
},
});
const {
@@ -1017,7 +1045,6 @@ const useDocumentsWorkspace = ({
documents: viewDocuments,
documentLookup,
folderNodes,
ensureFolderData,
detailPanelControlRef,
detailFolderFetchRef,
previewDocumentId,
@@ -1188,7 +1215,7 @@ const useDocumentsWorkspace = ({
handleDeleteSelection,
handleEntryPointerCore,
handleBulkSelectionReanalyze,
selectionValue: selection,
selectionValue: selectionState,
};
const documentMutations = {