feat: Implement new drag-and-drop system for documents and tags, including workspace-level handling and improved visual feedback.
This commit is contained in:
@@ -15,20 +15,13 @@ import {
|
||||
import AssetManager, { getAssetFromVersion } from '../../lib/assets/AssetManager';
|
||||
import useNotifyApiError from '../../hooks/useNotifyApiError';
|
||||
import TagManager from '../../lib/assets/TagManager';
|
||||
import { useManagementModals } from '../../app/useManagementModals';
|
||||
import { useAppDispatch, useAppState } from '../../lib/store/appState';
|
||||
import { fetchAsset, listFolderContents } from '../../lib/api/apiClient';
|
||||
import { useApi } from '../../lib/context/ApiContext';
|
||||
import { useWorkspaceSelection } from '../../app/useWorkspaceSelection';
|
||||
import { fetchAsset } from '../../lib/api/apiClient';
|
||||
import { useEntryPointer as useEntryPointerCore } from '../features/selection/useEntryPointer';
|
||||
import { isTagTransferEvent } from '../features/tagging/tagTransfer';
|
||||
import useDocumentsSelection from '../features/selection/useDocumentsSelection';
|
||||
import useBulkDocumentActions from './useBulkDocumentActions';
|
||||
import useDocumentPreview from '../../app/useDocumentPreview';
|
||||
import {
|
||||
DEFAULT_SORT_DIRECTION,
|
||||
DEFAULT_SORT_FIELD,
|
||||
createRootNode,
|
||||
mergeAssetIntoDocument,
|
||||
} from '../../app/workspaceUtils';
|
||||
import {
|
||||
@@ -57,6 +50,15 @@ import useDocumentCorrespondentActions from '../features/correspondents/useDocum
|
||||
import usePasskeys from '../../settings/usePasskeys';
|
||||
import { resolveBreadcrumbs } from '../logic/breadcrumbs';
|
||||
import useWorkspaceSelectionSync from '../features/selection/useWorkspaceSelectionSync';
|
||||
import useWorkspaceViewData from './useWorkspaceViewData';
|
||||
import useWorkspaceDragDrop from '../interactions/useWorkspaceDragDrop';
|
||||
import { useManagementModals } from '../../app/useManagementModals';
|
||||
import { useAppDispatch, useAppState } from '../../lib/store/appState';
|
||||
import { listFolderContents } from '../../lib/api/apiClient';
|
||||
import { useApi } from '../../lib/context/ApiContext';
|
||||
import { useWorkspaceSelection } from '../../app/useWorkspaceSelection';
|
||||
import useDocumentPreview from '../../app/useDocumentPreview';
|
||||
import { createRootNode } from '../../app/workspaceUtils';
|
||||
import type { DocumentId, FolderNodeId, Identifier } from '../../types/identifiers';
|
||||
|
||||
const EntryType = Object.freeze({
|
||||
@@ -148,24 +150,8 @@ const useDocumentsWorkspace = ({
|
||||
const [creatingFolder, setCreatingFolder] = useState(false);
|
||||
const { handleLogout } = useAuthManager({});
|
||||
|
||||
const tagRemovalCursorActiveRef = useRef(false);
|
||||
const tenantIdRef = useRef(currentTenantId);
|
||||
const detailPanelControlRef = useRef({ open: () => { }, close: () => { } });
|
||||
const setTagRemovalCursor = useCallback((active) => {
|
||||
if (tagRemovalCursorActiveRef.current === active) {
|
||||
return;
|
||||
}
|
||||
const body = document.body;
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
tagRemovalCursorActiveRef.current = active;
|
||||
if (active) {
|
||||
body.classList.add('desk-cursor-remove');
|
||||
} else {
|
||||
body.classList.remove('desk-cursor-remove');
|
||||
}
|
||||
}, []);
|
||||
const documentsRouteMatch = useMatch('/documents');
|
||||
const documentsFolderRouteMatch = useMatch('/documents/folder/:folderId');
|
||||
const documentsDetailRouteMatch = useMatch('/documents/:documentId');
|
||||
@@ -393,59 +379,18 @@ const useDocumentsWorkspace = ({
|
||||
});
|
||||
|
||||
const documentsFilter = documentsFilterValue;
|
||||
|
||||
const [visibleDocumentIds, setVisibleDocumentIds] = useState<DocumentId[]>([]);
|
||||
|
||||
const showingSearchResults = searchResultIds !== null;
|
||||
|
||||
useEffect(() => {
|
||||
const arraysEqual = (a: DocumentId[], b: DocumentId[]) =>
|
||||
a.length === b.length && a.every((value, index) => value === b[index]);
|
||||
|
||||
if (showingSearchResults && Array.isArray(searchResultIds)) {
|
||||
const ids = searchResultIds.filter((id): id is DocumentId => id != null);
|
||||
setVisibleDocumentIds((prev) => (arraysEqual(prev, ids) ? prev : ids));
|
||||
return;
|
||||
}
|
||||
|
||||
const folderIds = documents
|
||||
.map((doc) => (doc?.id ?? null) as DocumentId | null)
|
||||
.filter((id): id is DocumentId => id != null);
|
||||
setVisibleDocumentIds((prev) => (arraysEqual(prev, folderIds) ? prev : folderIds));
|
||||
}, [showingSearchResults, searchResultIds, documents]);
|
||||
|
||||
const viewDocuments = useMemo(
|
||||
() =>
|
||||
visibleDocumentIds
|
||||
.map((id) => documentLookup.get(id) || null)
|
||||
.filter((doc): doc is Document => Boolean(doc)),
|
||||
[visibleDocumentIds, documentLookup],
|
||||
);
|
||||
|
||||
const visibleDocumentKeys = useMemo(
|
||||
() => visibleDocumentIds.map((id) => createDocumentEntryKey(id)).filter(Boolean),
|
||||
[visibleDocumentIds],
|
||||
);
|
||||
|
||||
const visibleFolderKeys = useMemo(
|
||||
() =>
|
||||
showingSearchResults
|
||||
? []
|
||||
: (currentSubfolders || [])
|
||||
.map((folder) => createFolderEntryKey(folder.id))
|
||||
.filter(Boolean),
|
||||
[showingSearchResults, currentSubfolders],
|
||||
);
|
||||
|
||||
const visibleEntryKeys = useMemo(
|
||||
() => [...visibleFolderKeys, ...visibleDocumentKeys],
|
||||
[visibleFolderKeys, visibleDocumentKeys],
|
||||
);
|
||||
|
||||
const visibleEntryKeySet = useMemo(
|
||||
() => new Set(visibleEntryKeys),
|
||||
[visibleEntryKeys],
|
||||
);
|
||||
const {
|
||||
viewDocuments,
|
||||
visibleEntryKeySet,
|
||||
} = useWorkspaceViewData({
|
||||
documents,
|
||||
documentLookup,
|
||||
searchResultIds,
|
||||
showingSearchResults,
|
||||
currentSubfolders,
|
||||
});
|
||||
|
||||
const {
|
||||
openDocumentPreview,
|
||||
@@ -981,67 +926,9 @@ const useDocumentsWorkspace = ({
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [settingsOpen]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
setTagRemovalCursor(false);
|
||||
},
|
||||
[setTagRemovalCursor],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const host = shellRef.current;
|
||||
if (!host) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isTagTransfer = (event) => isTagTransferEvent(event);
|
||||
|
||||
const isDocumentDropTarget = (target) =>
|
||||
target instanceof Element ? Boolean(target.closest('[data-doc-id]')) : false;
|
||||
|
||||
const handleTagDragOver = (event) => {
|
||||
if (!isTagTransfer(event)) {
|
||||
return;
|
||||
}
|
||||
if (isDocumentDropTarget(event.target)) {
|
||||
setTagRemovalCursor(false);
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
setTagRemovalCursor(true);
|
||||
};
|
||||
|
||||
const handleTagDragLeave = (event) => {
|
||||
if (!isTagTransfer(event)) {
|
||||
return;
|
||||
}
|
||||
const related = event.relatedTarget;
|
||||
if (related instanceof Element && host.contains(related)) {
|
||||
if (isDocumentDropTarget(related)) {
|
||||
setTagRemovalCursor(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
setTagRemovalCursor(false);
|
||||
};
|
||||
|
||||
const handleTagDragEnd = () => {
|
||||
setTagRemovalCursor(false);
|
||||
};
|
||||
|
||||
host.addEventListener('dragover', handleTagDragOver, true);
|
||||
host.addEventListener('dragleave', handleTagDragLeave, true);
|
||||
window.addEventListener('dragend', handleTagDragEnd, true);
|
||||
|
||||
return () => {
|
||||
host.removeEventListener('dragover', handleTagDragOver, true);
|
||||
host.removeEventListener('dragleave', handleTagDragLeave, true);
|
||||
window.removeEventListener('dragend', handleTagDragEnd, true);
|
||||
setTagRemovalCursor(false);
|
||||
};
|
||||
}, [handleDocumentTagDetach, setTagRemovalCursor]);
|
||||
|
||||
useWorkspaceDragDrop({
|
||||
shellRef,
|
||||
});
|
||||
|
||||
const {
|
||||
detailPanelProps,
|
||||
|
||||
Reference in New Issue
Block a user