diff --git a/frontend/src/app/AppLayout.jsx b/frontend/src/app/AppLayout.jsx
index 4cfff69..016d43c 100644
--- a/frontend/src/app/AppLayout.jsx
+++ b/frontend/src/app/AppLayout.jsx
@@ -3132,7 +3132,66 @@ const AppLayout = () => {
);
if (!uniqueIds.length) return;
+ const uniqueIdSet = new Set(uniqueIds);
const target = targetFolderId === 'root' ? null : targetFolderId;
+ const targetLabel =
+ target === null
+ ? DEFAULT_FOLDER_NAME
+ : folderLabelMap.get(targetFolderId) || 'target folder';
+
+ const movedDocs = uniqueIds
+ .map((id) => {
+ const doc = documentLookup.get(id);
+ if (!doc) {
+ return null;
+ }
+ return {
+ id,
+ sourceFolderId: doc.folder_id ?? null,
+ document: doc,
+ };
+ })
+ .filter(Boolean);
+
+ const updatedDocsMap = new Map();
+ const resolveTargetName = () => {
+ if (!targetLabel) {
+ return null;
+ }
+ const segments = String(targetLabel).split('/');
+ return segments[segments.length - 1] || targetLabel;
+ };
+ const targetName = resolveTargetName();
+
+ movedDocs.forEach(({ id, document }) => {
+ if (!document) {
+ return;
+ }
+ const updated = {
+ ...document,
+ folder_id: target,
+ };
+ if (targetLabel) {
+ updated.folder_path = targetLabel;
+ if (targetName) {
+ updated.folder_name = targetName;
+ }
+ } else if (target === null) {
+ updated.folder_path = DEFAULT_FOLDER_NAME;
+ updated.folder_name = DEFAULT_FOLDER_NAME;
+ }
+ updatedDocsMap.set(id, updated);
+ });
+
+ const pruneRowCollection = (collection) =>
+ collection.filter((key) => {
+ if (!isDocumentRowKey(key)) {
+ return true;
+ }
+ const id = getRowId(key);
+ return id ? !uniqueIdSet.has(id) : true;
+ });
+
setLoading(true);
try {
if (uniqueIds.length === 1) {
@@ -3146,34 +3205,100 @@ const AppLayout = () => {
const count = uniqueIds.length;
const suffix = count === 1 ? '' : 's';
- const targetLabel =
- target === null
- ? DEFAULT_FOLDER_NAME
- : folderLabelMap.get(targetFolderId) || 'target folder';
- setStatusMessage(
- `Moved ${count} document${suffix} to ${targetLabel}.`,
- 'success',
- );
+ setStatusMessage(`Moved ${count} document${suffix} to ${targetLabel}.`, 'success');
- await refreshCurrentFolder();
- if (targetFolderId && targetFolderId !== selectedFolder) {
- await ensureFolderData(targetFolderId, { force: true, prefetchDepth: 1 });
+ if (updatedDocsMap.size) {
+ mapDocumentCaches((doc) => {
+ if (!doc || !uniqueIdSet.has(doc.id)) {
+ return doc;
+ }
+ const updated = updatedDocsMap.get(doc.id);
+ if (updated) {
+ return updated;
+ }
+ return { ...doc, folder_id: target };
+ });
+ } else {
+ mapDocumentCaches((doc) => {
+ if (!doc || !uniqueIdSet.has(doc.id)) {
+ return doc;
+ }
+ return { ...doc, folder_id: target };
+ });
+ }
+
+ if (uniqueIdSet.size) {
+ setDocuments((prev) => prev.filter((doc) => !uniqueIdSet.has(doc.id)));
+ setFolderContents((prev) => {
+ if (!prev.size) {
+ return prev;
+ }
+ let changed = false;
+ const next = new Map(prev);
+ movedDocs.forEach(({ id, sourceFolderId }) => {
+ const sourceKey = sourceFolderId || 'root';
+ 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;
+ });
+
+ setSelectedRowKeys((prev) => pruneRowCollection(prev));
+ setSelectionOrder((prev) => pruneRowCollection(prev));
+ selectionOrderRef.current = pruneRowCollection(selectionOrderRef.current);
+ if (
+ selectionAnchorRef.current &&
+ isDocumentRowKey(selectionAnchorRef.current) &&
+ uniqueIdSet.has(getRowId(selectionAnchorRef.current))
+ ) {
+ selectionAnchorRef.current = null;
+ }
+ if (focusedDocumentId && uniqueIdSet.has(focusedDocumentId)) {
+ setFocusedDocumentId(null);
+ }
+ if (
+ focusedRowKey &&
+ isDocumentRowKey(focusedRowKey) &&
+ uniqueIdSet.has(getRowId(focusedRowKey))
+ ) {
+ setFocusedRowKey(null);
+ }
+ }
+
+ if (targetFolderId && targetFolderId !== selectedFolder) {
+ await ensureFolderData(targetFolderId, { force: true, prefetchDepth: 1 });
+ }
+ } catch (error) {
+ const message = error.response?.data?.error || 'Failed to move documents.';
+ notifyApiError(error, message);
+ } finally {
+ setLoading(false);
}
- } catch (error) {
- const message = error.response?.data?.error || 'Failed to move documents.';
- notifyApiError(error, message);
- } finally {
- setLoading(false);
- }
- },
- [
- ensureFolderData,
- refreshCurrentFolder,
- selectedFolder,
- folderLabelMap,
- notifyApiError,
- setStatusMessage,
- ],
+ },
+ [
+ documentLookup,
+ ensureFolderData,
+ folderLabelMap,
+ focusedDocumentId,
+ mapDocumentCaches,
+ notifyApiError,
+ selectedFolder,
+ setDocuments,
+ setFolderContents,
+ setFocusedDocumentId,
+ focusedRowKey,
+ setFocusedRowKey,
+ setSelectionOrder,
+ setSelectedRowKeys,
+ setStatusMessage,
+ ],
);
const handleThumbnailRegeneration = useCallback(
diff --git a/frontend/src/documents/DocumentsTable.jsx b/frontend/src/documents/DocumentsTable.jsx
index 57be1e3..e086533 100644
--- a/frontend/src/documents/DocumentsTable.jsx
+++ b/frontend/src/documents/DocumentsTable.jsx
@@ -6,7 +6,7 @@ import {
EditIcon,
ViewListIcon,
ViewGridIcon,
- DesktopIcon,
+ IconFileStack,
FolderIcon,
TrashIcon,
RefreshIcon,
@@ -539,7 +539,7 @@ const DocumentsTable = ({
aria-pressed={isDeskView}
title="Desk view"
>
-
+
diff --git a/frontend/src/ui/icons.js b/frontend/src/ui/icons.js
index a68552c..3fbe1c3 100644
--- a/frontend/src/ui/icons.js
+++ b/frontend/src/ui/icons.js
@@ -1,3 +1,4 @@
+import { useId } from 'react';
import {
IconChevronRight as TablerChevronRight,
IconDownload as TablerDownload,
@@ -202,6 +203,42 @@ export const MinusVerticalIcon = ({ className, size = '1em', stroke = 1.6, ...re
/>
);
+export const IconFileStack = ({ className, size = 24, stroke = 160, ...rest }) => {
+ return (
+
+ );
+};
+
export const CloseIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (