feat: Implement new drag-and-drop system for documents and tags, including workspace-level handling and improved visual feedback.
This commit is contained in:
@@ -28,20 +28,20 @@ interface DesktopDocumentCardProps {
|
||||
selection: string[];
|
||||
requestCanvasFocus?: () => void;
|
||||
onTagDragEnter?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
|
||||
onTagDragOver?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
|
||||
onTagDragOver?: (event: React.DragEvent<HTMLDivElement>, doc: Document) => void;
|
||||
onTagDragLeave?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
|
||||
onTagDrop?: (event: React.DragEvent<HTMLDivElement>, doc: Document) => void;
|
||||
onDocTagDragStart?: (event: React.DragEvent<HTMLElement>, doc: Document, tag: any) => void;
|
||||
onDocTagDragEnd?: (event: React.DragEvent<HTMLElement>) => void;
|
||||
onTagDragStart?: (event: React.DragEvent<HTMLElement>, doc: Document, tag: any) => void;
|
||||
onTagDragEnd?: (event: React.DragEvent<HTMLElement>) => void;
|
||||
layoutCard: LayoutCard;
|
||||
}
|
||||
|
||||
const DesktopDocumentCard: React.FC<DesktopDocumentCardProps> = ({
|
||||
doc,
|
||||
style,
|
||||
shouldLoad,
|
||||
matchesFilter,
|
||||
selected,
|
||||
shouldLoad = false,
|
||||
matchesFilter = true,
|
||||
selected = false,
|
||||
docTagTokens,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
@@ -54,8 +54,8 @@ const DesktopDocumentCard: React.FC<DesktopDocumentCardProps> = ({
|
||||
onTagDragOver,
|
||||
onTagDragLeave,
|
||||
onTagDrop,
|
||||
onDocTagDragStart,
|
||||
onDocTagDragEnd,
|
||||
onTagDragStart,
|
||||
onTagDragEnd,
|
||||
layoutCard,
|
||||
}) => {
|
||||
const cardPointerHandlers = useCardPointer(
|
||||
@@ -89,27 +89,10 @@ const DesktopDocumentCard: React.FC<DesktopDocumentCardProps> = ({
|
||||
aria-hidden={ariaHidden}
|
||||
ref={(node) => layoutCard?.setRef(node)}
|
||||
{...cardPointerHandlers}
|
||||
onDragEnter={(event) => {
|
||||
if (doc?.id == null) {
|
||||
return;
|
||||
}
|
||||
onTagDragEnter?.(event, doc.id);
|
||||
}}
|
||||
onDragOver={(event) => {
|
||||
if (doc?.id == null) {
|
||||
return;
|
||||
}
|
||||
onTagDragOver?.(event, doc.id);
|
||||
}}
|
||||
onDragLeave={(event) => {
|
||||
if (doc?.id == null) {
|
||||
return;
|
||||
}
|
||||
onTagDragLeave?.(event, doc.id);
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
onTagDrop?.(event, doc);
|
||||
}}
|
||||
onDragEnter={(event) => onTagDragEnter?.(event, doc.id!)}
|
||||
onDragOver={(event) => onTagDragOver?.(event, doc)}
|
||||
onDragLeave={(event) => onTagDragLeave?.(event, doc.id!)}
|
||||
onDrop={(event) => onTagDrop?.(event, doc)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
preventAll(event);
|
||||
@@ -151,8 +134,8 @@ const DesktopDocumentCard: React.FC<DesktopDocumentCardProps> = ({
|
||||
title={tag.label}
|
||||
draggable
|
||||
data-desk-tag-chip="true"
|
||||
onDragStart={(event) => onDocTagDragStart?.(event, doc, tag)}
|
||||
onDragEnd={(event) => onDocTagDragEnd?.(event)}
|
||||
onDragStart={(event) => onTagDragStart?.(event, doc, tag)}
|
||||
onDragEnd={(event) => onTagDragEnd?.(event)}
|
||||
>
|
||||
<span className="tag-chip__label">{tag.label}</span>
|
||||
</span>
|
||||
|
||||
@@ -8,7 +8,10 @@ import React, {
|
||||
import { LayoutStore, LayoutCard } from '../logic/LayoutSystem';
|
||||
import DesktopDocumentCard from './DesktopDocumentCard';
|
||||
import usePreviewMetadata from '../hooks/usePreviewMetadata';
|
||||
import { useDeskTagInteractions } from '../tags/useDeskTagInteractions';
|
||||
import {
|
||||
TagDragHandlers,
|
||||
useTagInteractions,
|
||||
} from '../../documents/interactions/useTagInteractions';
|
||||
import './workspace-layout.css';
|
||||
import './workspace-items.css';
|
||||
import './workspace-cards.css';
|
||||
@@ -180,7 +183,7 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
|
||||
}, []);
|
||||
|
||||
// Tag Interactions
|
||||
const tagInteractions = useDeskTagInteractions({
|
||||
const tagDragHandlers: TagDragHandlers = useTagInteractions({
|
||||
onAssignTagToDocument: (docId: string, tagId: string) => {
|
||||
tags.onAttach?.(docId, tagId);
|
||||
},
|
||||
@@ -349,8 +352,6 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
|
||||
(e.target as Element).releasePointerCapture(e.pointerId);
|
||||
}
|
||||
}}
|
||||
onDrop={tagInteractions.handleCanvasDrop}
|
||||
onDragOver={tagInteractions.handleCanvasDragOver}
|
||||
>
|
||||
{isLayoutReady && items.map((doc, index) => {
|
||||
const docId = doc.id ? String(doc.id) : `temp-${index}`;
|
||||
@@ -391,12 +392,7 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
|
||||
openDocument(doc, isPreview ? 'preview' : 'sidepanel');
|
||||
}}
|
||||
layoutCard={layoutCard}
|
||||
onTagDragEnter={tagInteractions.handleTagDragEnterDoc}
|
||||
onTagDragOver={tagInteractions.handleTagDragOverDoc}
|
||||
onTagDragLeave={tagInteractions.handleTagDragLeaveDoc}
|
||||
onTagDrop={tagInteractions.handleTagDropOnDoc}
|
||||
onDocTagDragStart={tagInteractions.handleDocTagDragStart}
|
||||
onDocTagDragEnd={tagInteractions.handleDocTagDragEnd}
|
||||
{...tagDragHandlers}
|
||||
onSelect={(ids, extend = false) => {
|
||||
if (!extend) {
|
||||
handleSelectionChange(ids);
|
||||
|
||||
@@ -30,11 +30,10 @@
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.desk-item.is-tag-target .desk-item__card {
|
||||
outline: 0.35rem dashed var(--accent);
|
||||
outline-offset: 0.35rem;
|
||||
cursor: copy;
|
||||
}
|
||||
|
||||
.desk-item.is-tag-pending .desk-item__card {
|
||||
@@ -113,14 +112,9 @@
|
||||
.tag-chip--draggable {
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
cursor: grab;
|
||||
transition: transform 0.16s ease, opacity 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.tag-chip--draggable:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.tag-chip--draggable.is-drag-hidden {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { getTagColorStyle } from '../../utils/colors';
|
||||
import { writeTagTransferData } from '../features/tagging/tagTransfer';
|
||||
import type { DocumentTag } from '../../types/documents';
|
||||
import type { Identifier } from '../../types/identifiers';
|
||||
|
||||
@@ -9,8 +8,7 @@ interface DocumentTagsProps {
|
||||
tagLookupById?: Map<Identifier, DocumentTag> | null;
|
||||
onTagClick?: (tagId: Identifier) => void;
|
||||
docId: Identifier;
|
||||
onDocumentTagDetach?: (docId: Identifier, tagId: Identifier) => void;
|
||||
onTagDragStart?: (event: React.DragEvent<HTMLElement>, tagId: Identifier) => void;
|
||||
onTagDragStart?: (event: React.DragEvent<HTMLElement>, tag: DocumentTag) => void;
|
||||
onTagDragEnd?: (event: React.DragEvent<HTMLElement>) => void;
|
||||
}
|
||||
|
||||
@@ -19,7 +17,6 @@ const DocumentTags: React.FC<DocumentTagsProps> = ({
|
||||
tagLookupById,
|
||||
onTagClick,
|
||||
docId,
|
||||
onDocumentTagDetach,
|
||||
onTagDragStart,
|
||||
onTagDragEnd,
|
||||
}) => {
|
||||
@@ -58,24 +55,13 @@ const DocumentTags: React.FC<DocumentTagsProps> = ({
|
||||
} : undefined}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'copyMove';
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[documents] Failed to configure drag effect', error);
|
||||
}
|
||||
writeTagTransferData(event.dataTransfer, tag, docId);
|
||||
if (tagId) {
|
||||
onTagDragStart?.(event, tagId);
|
||||
// Let the hook handle the data transfer and UI
|
||||
if (tagId && onTagDragStart) {
|
||||
onTagDragStart(event, tag);
|
||||
}
|
||||
}}
|
||||
onDragEnd={(event) => {
|
||||
event.stopPropagation();
|
||||
if (event.dataTransfer.dropEffect === 'move' && tagId && onDocumentTagDetach) {
|
||||
onDocumentTagDetach(docId, tagId);
|
||||
}
|
||||
// Let the hook handle the cleanup and logic
|
||||
onTagDragEnd?.(event);
|
||||
}}
|
||||
onKeyDown={clickable ? (event) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ interface EntryTagsProps {
|
||||
onTagClick?: (tagId: Identifier) => void;
|
||||
docId: Identifier;
|
||||
onDocumentTagDetach?: (docId: Identifier, tagId: Identifier) => void;
|
||||
onTagDragStart?: (event: React.DragEvent<HTMLElement>, tagId: Identifier) => void;
|
||||
onTagDragStart?: (event: React.DragEvent<HTMLElement>, tag: DocumentTag) => void;
|
||||
onTagDragEnd?: (event: React.DragEvent<HTMLElement>) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,11 @@ interface DocumentsCommandContextValue {
|
||||
};
|
||||
tags: {
|
||||
onDrag: {
|
||||
start?: (event: DragEvent<HTMLElement>, docId: Identifier, tagId: Identifier) => void;
|
||||
start?: (event: DragEvent<HTMLElement>, doc: Document, tag: any) => void;
|
||||
end?: (event: DragEvent<HTMLElement>) => void;
|
||||
over?: (event: DragEvent<HTMLElement>, docId: Identifier) => void;
|
||||
leave?: (event: DragEvent<HTMLElement>) => void;
|
||||
over?: (event: DragEvent<HTMLElement>, doc: Document) => void;
|
||||
drop?: (event: DragEvent<HTMLElement>, doc: Document) => void;
|
||||
leave?: (event: DragEvent<HTMLElement>, doc: Document) => void;
|
||||
};
|
||||
onAttach?: (documentId: Identifier, tagId: Identifier) => void;
|
||||
onDetach?: (documentId: Identifier, tagId: Identifier) => void;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import type { DocumentId } from '../../types/identifiers';
|
||||
import type { Document } from '../../types/documents';
|
||||
import { createDocumentEntryKey, createFolderEntryKey } from '../../app/entryKey';
|
||||
|
||||
interface UseWorkspaceViewDataArgs {
|
||||
documents: Document[];
|
||||
documentLookup: Map<DocumentId, Document>;
|
||||
searchResultIds: DocumentId[] | null;
|
||||
showingSearchResults: boolean;
|
||||
currentSubfolders: any[];
|
||||
}
|
||||
|
||||
const useWorkspaceViewData = ({
|
||||
documents,
|
||||
documentLookup,
|
||||
searchResultIds,
|
||||
showingSearchResults,
|
||||
currentSubfolders,
|
||||
}: UseWorkspaceViewDataArgs) => {
|
||||
const [visibleDocumentIds, setVisibleDocumentIds] = useState<DocumentId[]>([]);
|
||||
|
||||
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: any) => createFolderEntryKey(folder.id))
|
||||
.filter(Boolean),
|
||||
[showingSearchResults, currentSubfolders],
|
||||
);
|
||||
|
||||
const visibleEntryKeys = useMemo(
|
||||
() => [...visibleFolderKeys, ...visibleDocumentKeys],
|
||||
[visibleFolderKeys, visibleDocumentKeys],
|
||||
);
|
||||
|
||||
const visibleEntryKeySet = useMemo(
|
||||
() => new Set(visibleEntryKeys),
|
||||
[visibleEntryKeys],
|
||||
);
|
||||
|
||||
return {
|
||||
viewDocuments,
|
||||
visibleDocumentIds,
|
||||
visibleEntryKeys,
|
||||
visibleEntryKeySet,
|
||||
};
|
||||
};
|
||||
|
||||
export default useWorkspaceViewData;
|
||||
@@ -36,11 +36,32 @@ const createTagTransferPayload = (
|
||||
};
|
||||
};
|
||||
|
||||
// Shared state to track dragged tag ID across components (Sidebar <-> Workspace)
|
||||
// This is necessary because dataTransfer payload is inaccessible during dragOver.
|
||||
interface ActiveDragState {
|
||||
tagId: TagId | null;
|
||||
sourceDocId: DocumentId | null;
|
||||
}
|
||||
|
||||
let activeDragState: ActiveDragState = { tagId: null, sourceDocId: null };
|
||||
|
||||
export const getActiveDragState = (): ActiveDragState => activeDragState;
|
||||
|
||||
export const clearTagTransferData = (): void => {
|
||||
activeDragState = { tagId: null, sourceDocId: null };
|
||||
};
|
||||
|
||||
export const writeTagTransferData = (
|
||||
dataTransfer: DataTransfer | null,
|
||||
tag: TagLike,
|
||||
sourceDocId: DocumentId | null = null,
|
||||
): void => {
|
||||
// Track globally for cursor logic
|
||||
activeDragState = {
|
||||
tagId: tag.id || null,
|
||||
sourceDocId: sourceDocId || null,
|
||||
};
|
||||
|
||||
if (!dataTransfer) {
|
||||
return;
|
||||
}
|
||||
@@ -56,7 +77,9 @@ export const writeTagTransferData = (
|
||||
}
|
||||
|
||||
try {
|
||||
TAG_MIME_TYPES.forEach((type) => dataTransfer.setData(type, serialized));
|
||||
TAG_MIME_TYPES.forEach((mime) => {
|
||||
dataTransfer.setData(mime, serialized);
|
||||
});
|
||||
if (payload.label) {
|
||||
dataTransfer.setData(TAG_TEXT_MIME_TYPE, payload.label);
|
||||
}
|
||||
|
||||
+69
-63
@@ -8,6 +8,8 @@ import {
|
||||
isTagTransferEvent,
|
||||
parseTagTransferPayload,
|
||||
writeTagTransferData,
|
||||
getActiveDragState,
|
||||
clearTagTransferData,
|
||||
} from '../../documents/features/tagging/tagTransfer';
|
||||
import type { Identifier } from '../../types/identifiers';
|
||||
import type { Document, DocumentTag } from '../../types/documents';
|
||||
@@ -42,7 +44,7 @@ const cleanupPreview = (previewNode: HTMLElement | null) => {
|
||||
}
|
||||
};
|
||||
|
||||
interface UseDeskTagInteractionsArgs {
|
||||
interface UseTagInteractionsArgs {
|
||||
onAssignTagToDocument?: (docId: Identifier, tagId: Identifier) => void;
|
||||
onRemoveTagFromDocument?: (docId: Identifier, tagId: Identifier) => void;
|
||||
requestCanvasFocus?: () => void;
|
||||
@@ -50,24 +52,28 @@ interface UseDeskTagInteractionsArgs {
|
||||
|
||||
interface DraggingTagState {
|
||||
element: HTMLElement | null;
|
||||
previewClone: HTMLElement | null;
|
||||
sourceDocId: Identifier;
|
||||
tagId: Identifier;
|
||||
initialX: number;
|
||||
initialY: number;
|
||||
distance: number;
|
||||
previewClone?: HTMLElement;
|
||||
}
|
||||
|
||||
export const useDeskTagInteractions = ({
|
||||
export interface TagDragHandlers {
|
||||
onTagDragEnter: (event: React.DragEvent<HTMLDivElement>, docId: Identifier) => void;
|
||||
onTagDragOver: (event: React.DragEvent<HTMLDivElement>, doc: Document) => void;
|
||||
onTagDragLeave: (event: React.DragEvent<HTMLDivElement>, docId: Identifier) => void;
|
||||
onTagDrop: (event: React.DragEvent<HTMLDivElement>, doc: Document) => void;
|
||||
onTagDragStart: (event: React.DragEvent<HTMLElement>, doc: Document, tag: DocumentTag) => void;
|
||||
onTagDragEnd: (event: React.DragEvent<HTMLElement>) => void;
|
||||
}
|
||||
|
||||
export const useTagInteractions = ({
|
||||
onAssignTagToDocument,
|
||||
onRemoveTagFromDocument,
|
||||
requestCanvasFocus,
|
||||
}: UseDeskTagInteractionsArgs) => {
|
||||
}: UseTagInteractionsArgs): TagDragHandlers => {
|
||||
const draggingTagRef = useRef<DraggingTagState | null>(null);
|
||||
|
||||
const isTagTransfer = useCallback((event: React.DragEvent) => isTagTransferEvent(event), []);
|
||||
|
||||
const handleTagDragEnterDoc = useCallback(
|
||||
const onTagDragEnter = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>, _docId: Identifier) => {
|
||||
if (!isTagTransfer(event)) return;
|
||||
preventAll(event);
|
||||
@@ -76,22 +82,43 @@ export const useDeskTagInteractions = ({
|
||||
[isTagTransfer],
|
||||
);
|
||||
|
||||
const handleTagDragOverDoc = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>, docId: Identifier) => {
|
||||
const onTagDragOver = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>, doc: Document) => {
|
||||
if (!doc || !doc.id) return;
|
||||
if (!isTagTransfer(event)) return;
|
||||
preventAll(event);
|
||||
event.currentTarget.classList.add('is-tag-target');
|
||||
|
||||
// Use shared state for all logic (Single Source of Truth)
|
||||
const { tagId: draggedTagId, sourceDocId: draggedSourceId } = getActiveDragState();
|
||||
|
||||
const isAssigned = doc.tags?.some((t) => t.id === draggedTagId);
|
||||
|
||||
if (event.dataTransfer) {
|
||||
// If dragging over source, copy (no removal). Else move (removal).
|
||||
const isSource = draggingTagRef.current?.sourceDocId === docId;
|
||||
event.dataTransfer.dropEffect = isSource ? 'copy' : 'move';
|
||||
const isSource = draggedSourceId === doc.id;
|
||||
|
||||
// Otherwise: separate document.
|
||||
if (isSource || isAssigned) {
|
||||
event.dataTransfer.dropEffect = 'none';
|
||||
event.currentTarget.classList.remove('is-tag-target');
|
||||
return;
|
||||
}
|
||||
|
||||
const isFromDocument = !!draggedSourceId;
|
||||
if (isFromDocument) {
|
||||
// Default to Move (transfer), allow Copy with Alt key
|
||||
event.dataTransfer.dropEffect = event.altKey ? 'copy' : 'move';
|
||||
} else {
|
||||
// Sidebar or external source: Copy only
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
|
||||
event.currentTarget.classList.add('is-tag-target');
|
||||
}
|
||||
},
|
||||
[isTagTransfer],
|
||||
);
|
||||
|
||||
const handleTagDragLeaveDoc = useCallback(
|
||||
const onTagDragLeave = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>, _docId: Identifier) => {
|
||||
if (!isTagTransfer(event)) return;
|
||||
// Ignore if leaving to a child element
|
||||
@@ -103,27 +130,7 @@ export const useDeskTagInteractions = ({
|
||||
[isTagTransfer],
|
||||
);
|
||||
|
||||
const handleCanvasDragOver = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>) => {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleCanvasDrop = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>) => {
|
||||
// Implicit removal via dragend (dropEffect='move')
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
preventAll(event);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleTagDropOnDoc = useCallback(
|
||||
const onTagDrop = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>, doc: Document) => {
|
||||
if (!doc || !doc.id) {
|
||||
return;
|
||||
@@ -134,20 +141,23 @@ export const useDeskTagInteractions = ({
|
||||
preventAll(event);
|
||||
event.currentTarget.classList.remove('is-tag-target');
|
||||
|
||||
// Add to target
|
||||
const payload = parseTagTransferPayload(event);
|
||||
if (payload && payload.sourceDocId === doc.id) {
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (!payload || !payload.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.sourceDocId === doc.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestCanvasFocus?.();
|
||||
|
||||
// Double-check assignment (even though cursor logic tries to prevent it)
|
||||
const isAssigned = doc.tags?.some((t) => t.id === payload.id);
|
||||
if (isAssigned) return;
|
||||
|
||||
if (onAssignTagToDocument && doc.id) {
|
||||
onAssignTagToDocument(doc.id, payload.id);
|
||||
}
|
||||
@@ -156,12 +166,12 @@ export const useDeskTagInteractions = ({
|
||||
[isTagTransfer, onAssignTagToDocument, requestCanvasFocus],
|
||||
);
|
||||
|
||||
const handleDocTagDragStart = useCallback(
|
||||
const onTagDragStart = useCallback(
|
||||
(event: React.DragEvent<HTMLElement>, doc: Document, tag: DocumentTag) => {
|
||||
console.log('[Tag] handleDocTagDragStart', { docId: doc?.id, tagId: tag?.id });
|
||||
if (!event?.dataTransfer || !doc?.id || !tag?.id) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.effectAllowed = 'copyMove';
|
||||
writeTagTransferData(event.dataTransfer, tag, doc.id);
|
||||
|
||||
@@ -179,20 +189,18 @@ export const useDeskTagInteractions = ({
|
||||
|
||||
draggingTagRef.current = {
|
||||
element,
|
||||
previewClone: clone || null,
|
||||
sourceDocId: doc.id,
|
||||
tagId: tag.id,
|
||||
initialX: pointerX,
|
||||
initialY: pointerY,
|
||||
distance: 0,
|
||||
previewClone: clone,
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDocTagDragEnd = useCallback(
|
||||
const onTagDragEnd = useCallback(
|
||||
(event: React.DragEvent<HTMLElement>) => {
|
||||
console.log('[Tag] handleDocTagDragEnd', { dropEffect: event?.dataTransfer?.dropEffect });
|
||||
event.stopPropagation();
|
||||
const { sourceDocId, tagId } = getActiveDragState();
|
||||
clearTagTransferData();
|
||||
|
||||
const dropEffect = event?.dataTransfer?.dropEffect;
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -206,8 +214,8 @@ export const useDeskTagInteractions = ({
|
||||
|
||||
// Remove if move operation completed
|
||||
if (dropEffect === 'move') {
|
||||
if (onRemoveTagFromDocument && state.sourceDocId && state.tagId) {
|
||||
onRemoveTagFromDocument(state.sourceDocId, state.tagId);
|
||||
if (onRemoveTagFromDocument && sourceDocId && tagId) {
|
||||
onRemoveTagFromDocument(sourceDocId, tagId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,13 +235,11 @@ export const useDeskTagInteractions = ({
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleTagDragEnterDoc,
|
||||
handleTagDragOverDoc,
|
||||
handleTagDragLeaveDoc,
|
||||
handleTagDropOnDoc,
|
||||
handleDocTagDragStart,
|
||||
handleDocTagDragEnd,
|
||||
handleCanvasDragOver,
|
||||
handleCanvasDrop,
|
||||
onTagDragEnter,
|
||||
onTagDragOver,
|
||||
onTagDragLeave,
|
||||
onTagDrop,
|
||||
onTagDragStart,
|
||||
onTagDragEnd,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useEffect } from 'react';
|
||||
import type { MutableRefObject } from 'react';
|
||||
import { isTagTransferEvent } from '../features/tagging/tagTransfer';
|
||||
|
||||
interface UseWorkspaceDragDropArgs {
|
||||
shellRef: MutableRefObject<HTMLElement | null>;
|
||||
}
|
||||
|
||||
const useWorkspaceDragDrop = ({
|
||||
shellRef,
|
||||
}: UseWorkspaceDragDropArgs) => {
|
||||
|
||||
useEffect(() => {
|
||||
const host = shellRef.current;
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow drop on workspace
|
||||
const handleTagDragOver = (event: DragEvent) => {
|
||||
if (!isTagTransferEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
|
||||
};
|
||||
|
||||
// Use bubbling (false) so children can stopPropagation
|
||||
host.addEventListener('dragover', handleTagDragOver, false);
|
||||
|
||||
return () => {
|
||||
host.removeEventListener('dragover', handleTagDragOver, false);
|
||||
};
|
||||
}, [shellRef]);
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export default useWorkspaceDragDrop;
|
||||
@@ -1,12 +1,10 @@
|
||||
import React, { type DragEvent } from 'react';
|
||||
import { parseTagTransferPayload } from '../features/tagging/tagTransfer';
|
||||
import { createDocumentEntryKey } from '../../app/entryKey';
|
||||
import { useDocumentOpen } from '../../lib/context/DocumentOpenContext';
|
||||
import type { Document } from '../../types/documents';
|
||||
import type { Document, DocumentTag } from '../../types/documents';
|
||||
import { useDocumentsCommandContext } from '../context/DocumentsCommandContext';
|
||||
import { useDocumentsViewStateContext } from '../context/DocumentsViewStateContext';
|
||||
import type { DocumentViewLogic } from './useDocumentViewLogic';
|
||||
import type { Identifier } from '../../types/identifiers';
|
||||
|
||||
interface UseDocumentItemLogicProps {
|
||||
doc: Document;
|
||||
@@ -30,8 +28,8 @@ export const useDocumentItemLogic = (props: UseDocumentItemLogicProps) => {
|
||||
end: onDocumentTagDragEnd,
|
||||
over: onDocumentTagDragOver,
|
||||
leave: onDocumentTagDragLeave,
|
||||
drop: onDocumentTagDrop,
|
||||
},
|
||||
onAttach: onDocumentTagAttach
|
||||
},
|
||||
onEntryPointer
|
||||
} = useDocumentsCommandContext();
|
||||
@@ -79,18 +77,11 @@ export const useDocumentItemLogic = (props: UseDocumentItemLogicProps) => {
|
||||
},
|
||||
onDragStart: (event: DragEvent<HTMLElement>) => onDocumentDragStart?.(event, doc),
|
||||
onDragEnd: (event: DragEvent<HTMLElement>) => onDocumentDragEnd?.(event),
|
||||
onDragOver: (event: DragEvent<HTMLElement>) => onDocumentTagDragOver?.(event, doc.id),
|
||||
onDragLeave: onDocumentTagDragLeave,
|
||||
onTagDragStart: (event: DragEvent<HTMLElement>, tagId: Identifier) => onDocumentTagDragStart?.(event, doc.id, tagId),
|
||||
onDragOver: (event: DragEvent<HTMLElement>) => onDocumentTagDragOver?.(event, doc),
|
||||
onDragLeave: (event: DragEvent<HTMLElement>) => onDocumentTagDragLeave?.(event, doc),
|
||||
onTagDragStart: (event: DragEvent<HTMLElement>, tag: DocumentTag) => onDocumentTagDragStart?.(event, doc, tag),
|
||||
onTagDragEnd: (event: DragEvent<HTMLElement>) => onDocumentTagDragEnd?.(event),
|
||||
onDrop: (event: DragEvent<HTMLElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const payload = parseTagTransferPayload(event);
|
||||
if (payload && payload.id && onDocumentTagAttach) {
|
||||
onDocumentTagAttach(doc.id, payload.id);
|
||||
}
|
||||
},
|
||||
onDrop: (event: DragEvent<HTMLElement>) => { onDocumentTagDrop?.(event, doc); },
|
||||
onRenameChange: setDocumentDraft,
|
||||
onRenameSubmit: () => submitDocumentEditing(doc),
|
||||
onRenameCancel: (event?: React.SyntheticEvent) => cancelDocumentEditing(event),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useMemo, useCallback, useRef } from 'react';
|
||||
import type { DocumentsPanelInnerProps } from './DocumentsPanel';
|
||||
import { isTagTransferEvent } from '../features/tagging/tagTransfer';
|
||||
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../features/selection/useEntryPointer';
|
||||
import { useDocumentsFilter } from '../context/DocumentsFilterContext';
|
||||
import { useWorkspaceSelectionContext } from '../../app/WorkspaceSelectionContext';
|
||||
import type { Identifier } from '../../types/identifiers';
|
||||
import { useTagInteractions } from '../interactions/useTagInteractions';
|
||||
|
||||
const EntryType = {
|
||||
folder: 'folder',
|
||||
@@ -37,6 +37,12 @@ export const useDocumentsContextValues = (props: DocumentsPanelInnerProps) => {
|
||||
toggleCorrespondent: toggleCorrespondentFilter,
|
||||
} = useDocumentsFilter();
|
||||
|
||||
// Handlers
|
||||
const tagDragHandlers = useTagInteractions({
|
||||
onAssignTagToDocument: props.onDocumentTagAttach,
|
||||
onRemoveTagFromDocument: props.onDocumentTagDetach,
|
||||
});
|
||||
|
||||
// Derived State
|
||||
const draggingDocumentIdsSet = useMemo(
|
||||
() => new Set(draggingDocumentIds || []),
|
||||
@@ -69,52 +75,6 @@ export const useDocumentsContextValues = (props: DocumentsPanelInnerProps) => {
|
||||
// Refs
|
||||
const scrollRef = useRef<HTMLElement | null>(null);
|
||||
const suppressDocumentClickRef = useRef(false);
|
||||
const draggingTagRef = useRef<{ docId: Identifier; tagId: Identifier } | null>(null);
|
||||
|
||||
// Handlers
|
||||
const isTagDragEvent = useCallback((event: any) => isTagTransferEvent(event), []);
|
||||
|
||||
const handleDocumentTagDragStart = useCallback(
|
||||
(_event: any, docId: Identifier, tagId: Identifier) => {
|
||||
draggingTagRef.current = { docId, tagId };
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDocumentTagDragEnd = useCallback(
|
||||
(_event: any) => {
|
||||
draggingTagRef.current = null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDocumentTagDragOver = useCallback(
|
||||
(event: any, docId: Identifier) => {
|
||||
if (!isTagDragEvent(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
const isSource = draggingTagRef.current?.docId === docId;
|
||||
event.dataTransfer.dropEffect = isSource ? 'copy' : 'move';
|
||||
|
||||
event.currentTarget.classList.add('tag-drop-target');
|
||||
},
|
||||
[isTagDragEvent],
|
||||
);
|
||||
|
||||
const handleDocumentTagDragLeave = useCallback(
|
||||
(event: any) => {
|
||||
if (!isTagDragEvent(event)) {
|
||||
return;
|
||||
}
|
||||
if (event.relatedTarget && event.currentTarget.contains(event.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.classList.remove('tag-drop-target');
|
||||
},
|
||||
[isTagDragEvent],
|
||||
);
|
||||
|
||||
const handleFolderClick = useCallback(
|
||||
(folder: any, event: any) => {
|
||||
@@ -187,10 +147,7 @@ export const useDocumentsContextValues = (props: DocumentsPanelInnerProps) => {
|
||||
handleFolderClick,
|
||||
handleDocumentDragStartLocal,
|
||||
handleDocumentDragEndLocal,
|
||||
handleDocumentTagDragStart,
|
||||
handleDocumentTagDragEnd,
|
||||
handleDocumentTagDragOver,
|
||||
handleDocumentTagDragLeave,
|
||||
tagDragHandlers,
|
||||
toggleTagFilter,
|
||||
toggleCorrespondentFilter,
|
||||
});
|
||||
@@ -201,10 +158,7 @@ export const useDocumentsContextValues = (props: DocumentsPanelInnerProps) => {
|
||||
handleFolderClick,
|
||||
handleDocumentDragStartLocal,
|
||||
handleDocumentDragEndLocal,
|
||||
handleDocumentTagDragStart,
|
||||
handleDocumentTagDragEnd,
|
||||
handleDocumentTagDragOver,
|
||||
handleDocumentTagDragLeave,
|
||||
tagDragHandlers,
|
||||
toggleTagFilter,
|
||||
toggleCorrespondentFilter,
|
||||
};
|
||||
@@ -231,10 +185,11 @@ export const useDocumentsContextValues = (props: DocumentsPanelInnerProps) => {
|
||||
},
|
||||
tags: {
|
||||
onDrag: {
|
||||
start: (e: any, d: any, t: any) => latestHandlersRef.current.handleDocumentTagDragStart(e, d, t),
|
||||
end: (e: any) => latestHandlersRef.current.handleDocumentTagDragEnd(e),
|
||||
over: (e: any, d: any) => latestHandlersRef.current.handleDocumentTagDragOver(e, d),
|
||||
leave: (e: any) => latestHandlersRef.current.handleDocumentTagDragLeave(e),
|
||||
start: (e: any, d: any, t: any) => latestHandlersRef.current.tagDragHandlers.onTagDragStart(e, d, t),
|
||||
end: (e: any) => latestHandlersRef.current.tagDragHandlers.onTagDragEnd(e),
|
||||
over: (e: any, d: any) => latestHandlersRef.current.tagDragHandlers.onTagDragOver(e, d),
|
||||
leave: (e: any, d: any) => latestHandlersRef.current.tagDragHandlers.onTagDragLeave(e, d.id),
|
||||
drop: (e: any, d: any) => latestHandlersRef.current.tagDragHandlers.onTagDrop(e, d),
|
||||
},
|
||||
onAttach: (d: any, t: any) => latestPropsRef.current.onDocumentTagAttach?.(d, t),
|
||||
onDetach: (d: any, t: any) => latestPropsRef.current.onDocumentTagDetach?.(d, t),
|
||||
|
||||
@@ -698,17 +698,17 @@
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.documents-panel tbody tr.document.tag-drop-target {
|
||||
.documents-panel tbody tr.document.is-tag-target {
|
||||
background: var(--accent-soft);
|
||||
box-shadow: inset 0 0 0 2px var(--accent);
|
||||
}
|
||||
|
||||
.document-card.tag-drop-target {
|
||||
.document-card.is-tag-target {
|
||||
box-shadow: 0 0 0 2px var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.document-card.tag-drop-target .document-card__title {
|
||||
.document-card.is-tag-target .document-card__title {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { PlusIcon, SettingsIcon } from '../../components/icons';
|
||||
import { getTagColorStyle } from '../../utils/colors';
|
||||
import { writeTagTransferData, clearTagTransferData } from '../../documents/features/tagging/tagTransfer';
|
||||
import type { Identifier } from '../../types/identifiers';
|
||||
import { useDocumentsFilter } from '../../documents/context/DocumentsFilterContext';
|
||||
|
||||
@@ -120,18 +121,13 @@ const SidebarTagList: React.FC<SidebarTagListProps> = ({
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
try {
|
||||
const payload = JSON.stringify({
|
||||
id: tag.id,
|
||||
label: tag.label,
|
||||
color: tag.color || null,
|
||||
});
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('application/x-papercrate-tag', payload);
|
||||
event.dataTransfer.setData('text/papercrate-tag', payload);
|
||||
writeTagTransferData(event.dataTransfer, tag);
|
||||
} catch (error) {
|
||||
console.warn('[sidebar] Failed to set tag drag payload', error);
|
||||
}
|
||||
}}
|
||||
onDragEnd={clearTagTransferData}
|
||||
>
|
||||
{tag.label}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user