diff --git a/frontend/src/DesktopWorkspace.css b/frontend/src/DesktopWorkspace.css
index 6489c37..4aecad2 100644
--- a/frontend/src/DesktopWorkspace.css
+++ b/frontend/src/DesktopWorkspace.css
@@ -111,6 +111,39 @@
transition: transform 0.28s ease;
}
+.desk-item__correspondents {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ align-items: flex-start;
+ transform-origin: bottom left;
+ transform: translate(0.5em, -0.5em);
+ pointer-events: none;
+}
+
+.desk-correspondent-chip {
+ pointer-events: none;
+ font-size: 0.82rem;
+ padding: 0.18rem 0.55rem;
+ max-width: min(16rem, 80%);
+ display: inline-flex;
+ align-items: center;
+ overflow: hidden;
+ box-shadow: 2px 2px 4px var(--shadow-faint);
+ background: color-mix(in oklch, var(--surface-subtle) 88%, transparent);
+ color: var(--muted);
+}
+
+.desk-correspondent-chip__label {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.tag-chip--draggable {
user-select: none;
diff --git a/frontend/src/DesktopWorkspace.jsx b/frontend/src/DesktopWorkspace.jsx
index 6c50014..3df97ea 100644
--- a/frontend/src/DesktopWorkspace.jsx
+++ b/frontend/src/DesktopWorkspace.jsx
@@ -12,6 +12,7 @@ import { useAssetNavigator } from './hooks/useAssetNavigator';
import { ArrowLeftIcon, ArrowRightIcon, CloseIcon } from './ui/icons';
import { createDocumentsTableHeaderActions } from './documents/DocumentsPanel';
import createWorkspaceSurfaceConfig from './documents/workspaceHeader';
+import { resolveCorrespondents } from './documents/correspondents';
import DetailPanel from './detail/DetailPanel';
import { clamp, formatTransform } from './desktop/math';
import { preventAll } from './desktop/events';
@@ -2204,6 +2205,7 @@ const DesktopWorkspaceView = () => {
const shouldLoad = docKey ? visibleDocIds.has(docKey) : false;
const dragging = draggingId === doc.id;
const tags = Array.isArray(doc.tags) ? doc.tags : [];
+ const correspondents = resolveCorrespondents(doc);
const docTagKeys = tags
.map((tag) => (tag ? tag.id : null))
.filter(Boolean);
@@ -2298,6 +2300,21 @@ const DesktopWorkspaceView = () => {
onNavigatorSnapshot={handleNavigatorSnapshot}
shouldLoad={shouldLoad}
/>
+ {correspondents.length > 0 && (
+
+ {correspondents.map((correspondent) => (
+
+
+ {correspondent.name}
+
+
+ ))}
+
+ )}
{tags.length > 0 && (
{tags.map((tag) => {
diff --git a/frontend/src/app/AppLayout.jsx b/frontend/src/app/AppLayout.jsx
index e29488e..0c79ee7 100644
--- a/frontend/src/app/AppLayout.jsx
+++ b/frontend/src/app/AppLayout.jsx
@@ -371,7 +371,7 @@ const AppLayout = () => {
setFocusedRowKey,
applySelection,
clearSelection: clearSelectionInternal,
- handleRowSelection: handleRowSelectionInternal,
+ handleEntrySelection: handleEntrySelectionInternal,
promoteSelectionOrder: promoteSelectionOrderInternal,
configureSelectionEnvironment,
} = useDocumentSelection({
@@ -784,44 +784,6 @@ const AppLayout = () => {
[mapDocumentCaches],
);
- const removeDocumentFromCaches = useCallback(
- (documentId) => {
- if (!documentId) {
- return;
- }
-
- const removeFromList = (list) => {
- const next = list.filter((doc) => doc.id !== documentId);
- return next.length === list.length ? list : next;
- };
-
- setDocuments((prev) => removeFromList(prev));
- setSearchResults((prev) => (Array.isArray(prev) ? removeFromList(prev) : prev));
- setFolderContents((prev) => {
- if (!prev.size) {
- return prev;
- }
- let changed = false;
- const next = new Map();
- prev.forEach((contents, key) => {
- const docs = Array.isArray(contents?.documents) ? contents.documents : null;
- if (!docs || docs.length === 0) {
- next.set(key, contents);
- return;
- }
- const filteredDocs = docs.filter((doc) => doc.id !== documentId);
- if (filteredDocs.length !== docs.length) {
- changed = true;
- next.set(key, { ...contents, documents: filteredDocs });
- } else {
- next.set(key, contents);
- }
- });
- return changed ? next : prev;
- });
- },
- [setDocuments, setSearchResults, setFolderContents],
- );
const folderOptions = useMemo(() => {
@@ -900,11 +862,11 @@ const AppLayout = () => {
});
}, [configureSelectionEnvironment, visibleRowKeySet, navigableRowKeys]);
- const handleRowSelection = useCallback(
+ const handleEntrySelection = useCallback(
(rowKey, event) => {
- handleRowSelectionInternal(rowKey, event);
+ handleEntrySelectionInternal(rowKey, event);
},
- [handleRowSelectionInternal],
+ [handleEntrySelectionInternal],
);
const promoteSelectionOrder = useCallback(
@@ -2462,7 +2424,7 @@ const AppLayout = () => {
if (!isAlreadySelected && folderKey) {
effectiveFolderSelection = [folderId];
effectiveDocumentSelection = [];
- handleRowSelection(folderKey, { preventDefault: () => {} });
+ handleEntrySelection(folderKey, { preventDefault: () => {} });
}
const uniqueFolders = effectiveFolderSelection.length
@@ -2513,7 +2475,7 @@ const AppLayout = () => {
selectedDocumentIds,
setDraggedFolderId,
setDraggedDocumentIds,
- handleRowSelection,
+ handleEntrySelection,
documentLookup,
createDragPreview,
],
@@ -3150,7 +3112,7 @@ const AppLayout = () => {
if (!row) {
return;
}
- handleRowSelection(row.key, event);
+ handleEntrySelection(row.key, event);
if (row.type === 'folder') {
selectFolder(row.id);
} else if (row.type === 'document') {
@@ -3163,7 +3125,7 @@ const AppLayout = () => {
if (key === 'ArrowDown') {
if (initializedFromEmptyState && selectedEntries.length === 0) {
- handleRowSelection(activeKey, {
+ handleEntrySelection(activeKey, {
shiftKey,
preventDefault: () => {},
});
@@ -3172,7 +3134,7 @@ const AppLayout = () => {
nextIndex = currentIndex === -1 ? 0 : Math.min(currentIndex + 1, navigableRows.length - 1);
} else if (key === 'ArrowUp') {
if (initializedFromEmptyState && selectedEntries.length === 0) {
- handleRowSelection(activeKey, {
+ handleEntrySelection(activeKey, {
shiftKey,
preventDefault: () => {},
});
@@ -3200,7 +3162,7 @@ const AppLayout = () => {
setFocusedRowKey(targetRow.key);
- handleRowSelection(targetRow.key, {
+ handleEntrySelection(targetRow.key, {
shiftKey,
preventDefault: () => {},
});
@@ -3210,7 +3172,7 @@ const AppLayout = () => {
navigableRowKeys,
focusedRowKey,
selectedEntries,
- handleRowSelection,
+ handleEntrySelection,
selectFolder,
openDocumentPreview,
setFocusedRowKey,
@@ -4163,76 +4125,6 @@ const AppLayout = () => {
return list.find((doc) => doc.id === focusedDocumentId) || null;
}, [searchResults, documents, focusedDocumentId]);
- const handleDocumentDelete = useCallback(
- async (documentId) => {
- if (!documentId) return;
- if (!token) {
- setStatusMessage('Log in to manage documents.', 'error');
- return;
- }
-
- const doc = documentLookup.get(documentId) || null;
- const label = doc?.title;
-
- const confirmed = window.confirm(`Move "${label}" to trash? You can restore it from trash later.`);
- if (!confirmed) {
- return;
- }
-
- setLoading(true);
- try {
- await api.delete(`/documents/${documentId}`);
-
- removeDocumentFromCaches(documentId);
-
- setPreviewEntries((prev) => {
- if (!prev.has(documentId)) {
- return prev;
- }
- const next = new Map(prev);
- next.delete(documentId);
- return next;
- });
- previewInflightRef.current.delete(documentId);
-
- if (selectedDocumentIds.includes(documentId)) {
- const remainingRowKeys = selectedDocumentIds
- .filter((id) => id !== documentId)
- .map((id) => resolveDocumentRowKey(id))
- .filter(Boolean);
- const removedKey = resolveDocumentRowKey(documentId);
- applySelection(remainingRowKeys, {
- anchor: null,
- interactedKeys: removedKey ? [removedKey] : [],
- });
- }
-
- if (previewDocumentId === documentId) {
- closeDocumentPreview();
- }
-
- setStatusMessage('Document deleted.', 'success');
- } catch (error) {
- const message = error.response?.data?.error || 'Failed to delete document.';
- notifyApiError(error, message);
- } finally {
- setLoading(false);
- }
- },
- [
- token,
- documentLookup,
- setStatusMessage,
- removeDocumentFromCaches,
- setPreviewEntries,
- previewInflightRef,
- previewDocumentId,
- closeDocumentPreview,
- selectedDocumentIds,
- applySelection,
- notifyApiError,
- ],
- );
const orderedSelectedDocuments = useMemo(() => {
const ordered = [];
@@ -4591,10 +4483,8 @@ const AppLayout = () => {
onFolderDragStart: handleFolderDragStart,
onFolderDragEnd: handleFolderDragEnd,
draggedFolderId,
- onFolderDelete: handleFolderDelete,
onFolderRename: handleFolderRename,
onDocumentOpen: openDocumentPreview,
- onDocumentDelete: handleDocumentDelete,
onDocumentRename: handleDocumentTitleUpdate,
selectedDocumentIds,
selectedFolderIds,
@@ -4610,17 +4500,13 @@ const AppLayout = () => {
onFocusedRowChange: setFocusedRowKey,
ensureAssetUrl,
getDocumentAsset,
- getDownloadHref: (doc) =>
- doc?.current_version?.download_path
- ? resolveApiPath(doc.current_version.download_path)
- : null,
onTagClick: toggleTagFilter,
onCorrespondentClick: toggleCorrespondentFilter,
onDocumentTagDrop: handleDocumentTagDrop,
viewMode: documentsViewMode,
onViewModeChange: handleDocumentsViewModeChange,
onClearSelection: clearDocumentSelection,
- onRowSelection: handleRowSelection,
+ onEntrySelection: handleEntrySelection,
onOpenDetailPanel: openDetailPanel,
}),
[
@@ -4635,7 +4521,6 @@ const AppLayout = () => {
draggedFolderId,
focusedRowKey,
folderClickHandlers,
- handleDocumentDelete,
handleDocumentDragEnd,
handleDocumentDragStart,
handleDocumentListFocus,
@@ -4643,11 +4528,10 @@ const AppLayout = () => {
handleDocumentTagDrop,
handleDocumentTitleUpdate,
handleDocumentsViewModeChange,
- handleFolderDelete,
handleFolderDragEnd,
handleFolderDragStart,
handleFolderRename,
- handleRowSelection,
+ handleEntrySelection,
isFilterActive,
openDetailPanel,
openDocumentPreview,
@@ -4715,9 +4599,9 @@ const AppLayout = () => {
currentTenantId,
folderClickHandlers,
folderNodes,
- handleFolderDelete,
handleFolderDragEnd,
handleFolderDragStart,
+ handleFolderDelete,
handleFolderRename,
handleLogout,
handleSearchChange,
@@ -4830,9 +4714,9 @@ const AppLayout = () => {
if (!rowKey) {
return;
}
- handleRowSelection(rowKey, event);
+ handleEntrySelection(rowKey, event);
},
- [handleRowSelection],
+ [handleEntrySelection],
);
const handleDeskDocumentStackSelect = useCallback(
diff --git a/frontend/src/app/useDocumentSelection.js b/frontend/src/app/useDocumentSelection.js
index 171ff8e..16f3b83 100644
--- a/frontend/src/app/useDocumentSelection.js
+++ b/frontend/src/app/useDocumentSelection.js
@@ -143,7 +143,7 @@ export const useDocumentSelection = ({
applySelection([], { anchor: null, interactedKeys: [] });
}, [applySelection]);
- const handleRowSelection = useCallback(
+ const handleEntrySelection = useCallback(
(rowKey, event) => {
const visibleRowKeySet = visibleRowKeySetRef.current;
const navigableRowKeys = navigableRowKeysRef.current;
@@ -241,7 +241,7 @@ export const useDocumentSelection = ({
setFocusedRowKey,
applySelection,
clearSelection,
- handleRowSelection,
+ handleEntrySelection,
promoteSelectionOrder,
configureSelectionEnvironment,
};
diff --git a/frontend/src/documents/DocumentsGrid.jsx b/frontend/src/documents/DocumentsGrid.jsx
index 94fdd56..9c56ea6 100644
--- a/frontend/src/documents/DocumentsGrid.jsx
+++ b/frontend/src/documents/DocumentsGrid.jsx
@@ -1,10 +1,11 @@
import React from 'react';
-import { FolderIcon } from '../ui/icons';
+import { FolderIcon, CheckIcon, CloseIcon } from '../ui/icons';
import DocumentThumbnailImage from './DocumentThumbnailImage';
import CorrespondentLinks from './CorrespondentLinks';
import { getTagColorStyle } from '../utils/colors';
import { resolveCorrespondents } from './correspondents';
import { writeTagTransferData } from './tagTransfer';
+import useInlineRename from './useInlineRename';
const DocumentsGrid = ({
entries,
@@ -35,7 +36,42 @@ const DocumentsGrid = ({
onCorrespondentClick,
activeCorrespondentIdSet,
onClearSelection,
-}) => (
+ onDocumentRename,
+ onFolderRename,
+}) => {
+ const {
+ editingId: editingDocumentId,
+ draftValue: documentDraft,
+ setDraftValue: setDocumentDraft,
+ beginEditing: beginDocumentEditing,
+ cancelEditing: cancelDocumentEditing,
+ submitEditing: submitDocumentEditing,
+ savingId: savingDocumentId,
+ attachInputRef: attachDocumentInputRef,
+ } = useInlineRename(onDocumentRename, {
+ getCurrentValue: (doc) => doc?.title ?? '',
+ getEntityId: (doc) => doc?.id ?? null,
+ });
+
+ const {
+ editingId: editingFolderId,
+ draftValue: folderDraft,
+ setDraftValue: setFolderDraft,
+ beginEditing: beginFolderEditing,
+ cancelEditing: cancelFolderEditing,
+ submitEditing: submitFolderEditing,
+ savingId: savingFolderId,
+ attachInputRef: attachFolderInputRef,
+ } = useInlineRename(onFolderRename, {
+ getCurrentValue: (folder) => folder?.name ?? '',
+ getEntityId: (folder) => folder?.id ?? null,
+ });
+
+ const documentSelectionCount = selectedDocumentIdsSet?.size ?? 0;
+ const folderSelectionCount = selectedFolderIdsSet?.size ?? 0;
+ const totalSelectionCount = documentSelectionCount + folderSelectionCount;
+
+ return (
0 && trimmedFolderDraft !== folder.name;
+ const allowInlineFolderEdit = canRenameFolder && isSelectedFolder && totalSelectionCount === 1;
return (
-
- {folder.name}
-
+ {isFolderEditing ? (
+
+ setFolderDraft(event.target.value)}
+ onClick={(event) => event.stopPropagation()}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ submitFolderEditing(folder);
+ } else if (event.key === 'Escape') {
+ event.preventDefault();
+ cancelFolderEditing(event);
+ }
+ }}
+ onBlur={(event) => {
+ const nextFocus = event.relatedTarget;
+ if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) {
+ cancelFolderEditing();
+ }
+ }}
+ />
+
+
+
+ ) : (
+
+ {
+ if (!allowInlineFolderEdit) {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ beginFolderEditing(folder);
+ }}
+ onKeyDown={(event) => {
+ if (!allowInlineFolderEdit) {
+ return;
+ }
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ event.stopPropagation();
+ beginFolderEditing(folder);
+ }
+ }}
+ >
+ {folder.name}
+
+
+ )}
);
@@ -111,6 +229,13 @@ const DocumentsGrid = ({
const cardClasses = ['document-card', 'document'];
if (isSelected) cardClasses.push('selected');
if (isDraggingDoc) cardClasses.push('is-dragging');
+ const isEditingDoc = editingDocumentId === doc.id;
+ const documentDraftValue = isEditingDoc ? documentDraft : doc.title;
+ const trimmedDocumentDraft = isEditingDoc ? documentDraft.trim() : '';
+ const isDocumentSaving = savingDocumentId === doc.id;
+ const canSubmitDocument =
+ isEditingDoc && trimmedDocumentDraft.length > 0 && trimmedDocumentDraft !== doc.title;
+ const allowInlineDocumentEdit = onDocumentRename && isSelected && totalSelectionCount === 1;
return (