tags drags
This commit is contained in:
@@ -3,6 +3,8 @@ import { resolveDocumentAssetUrl } from '../asset_manager';
|
||||
import { getTagColorStyle } from '../utils/colors';
|
||||
import { DownloadIcon, FolderIcon, EditIcon } from '../ui/icons';
|
||||
|
||||
const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
|
||||
|
||||
const DocumentThumbnailImage = ({ document, ensureAssetUrl, getDocumentAsset, alt }) => {
|
||||
const url = useMemo(
|
||||
() =>
|
||||
@@ -71,6 +73,7 @@ const DocumentsTable = ({
|
||||
getDownloadHref,
|
||||
onTagClick,
|
||||
isSearchLoading = false,
|
||||
onDocumentTagDrop,
|
||||
}) => {
|
||||
const showingSearchResults = searchResults !== null;
|
||||
const rows = showingSearchResults ? searchResults : documents;
|
||||
@@ -88,6 +91,10 @@ const DocumentsTable = ({
|
||||
[draggingDocumentIds],
|
||||
);
|
||||
const scrollRef = useRef(null);
|
||||
const isTagDragEvent = useCallback((event) => {
|
||||
const types = Array.from(event.dataTransfer?.types || []);
|
||||
return TAG_MIME_TYPES.some((type) => types.includes(type));
|
||||
}, []);
|
||||
const ensureFocusedRowVisible = useCallback(() => {
|
||||
if (!focusedRowKey) return;
|
||||
const container = scrollRef.current;
|
||||
@@ -139,6 +146,58 @@ const DocumentsTable = ({
|
||||
return undefined;
|
||||
}, [focusedRowKey]);
|
||||
|
||||
const handleDocumentTagDragOver = useCallback(
|
||||
(event) => {
|
||||
if (!isTagDragEvent(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
event.currentTarget.classList.add('tag-drop-target');
|
||||
},
|
||||
[isTagDragEvent],
|
||||
);
|
||||
|
||||
const handleDocumentTagDragLeave = useCallback(
|
||||
(event) => {
|
||||
if (!isTagDragEvent(event)) {
|
||||
return;
|
||||
}
|
||||
if (event.relatedTarget && event.currentTarget.contains(event.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.classList.remove('tag-drop-target');
|
||||
},
|
||||
[isTagDragEvent],
|
||||
);
|
||||
|
||||
const handleDocumentTagDrop = useCallback(
|
||||
(event, documentId) => {
|
||||
if (!isTagDragEvent(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.currentTarget.classList.remove('tag-drop-target');
|
||||
const payload =
|
||||
event.dataTransfer.getData('application/x-papercrate-tag') ||
|
||||
event.dataTransfer.getData('text/papercrate-tag');
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(payload);
|
||||
if (parsed?.id && onDocumentTagDrop) {
|
||||
onDocumentTagDrop(documentId, parsed);
|
||||
}
|
||||
} catch (
|
||||
// eslint-disable-next-line no-empty
|
||||
error
|
||||
) {}
|
||||
},
|
||||
[isTagDragEvent, onDocumentTagDrop],
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="documents-panel column">
|
||||
<div className="column-header">
|
||||
@@ -336,6 +395,9 @@ const DocumentsTable = ({
|
||||
draggable
|
||||
onDragStart={(event) => onDocumentDragStart(event, doc)}
|
||||
onDragEnd={onDocumentDragEnd}
|
||||
onDragOver={handleDocumentTagDragOver}
|
||||
onDragLeave={handleDocumentTagDragLeave}
|
||||
onDrop={(event) => handleDocumentTagDrop(event, doc.id)}
|
||||
>
|
||||
<td className="thumb-cell">
|
||||
<DocumentThumbnailImage
|
||||
@@ -393,6 +455,29 @@ const DocumentsTable = ({
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'copyMove';
|
||||
}
|
||||
const payload = JSON.stringify({
|
||||
id: tag.id,
|
||||
label: tag.label,
|
||||
sourceDocId: doc.id,
|
||||
});
|
||||
event.dataTransfer?.setData('application/x-papercrate-tag', payload);
|
||||
event.dataTransfer?.setData('text/papercrate-tag', payload);
|
||||
event.dataTransfer?.setData('text/plain', tag.label || 'Tag');
|
||||
} catch (
|
||||
// eslint-disable-next-line no-empty
|
||||
error
|
||||
) {}
|
||||
}}
|
||||
onDragEnd={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
|
||||
+307
-141
@@ -41,6 +41,7 @@ const runtimeApiBase =
|
||||
|
||||
const DEFAULT_DEV_API = 'http://127.0.0.1:3000';
|
||||
const ASSET_PRESIGN_TTL_MS = 240 * 1000; // backend issues 5 min tokens; refresh slightly early
|
||||
const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
|
||||
|
||||
const API_ROOT = (runtimeApiBase || process.env.API_BASE_URL || DEFAULT_DEV_API).replace(/\/$/, '');
|
||||
|
||||
@@ -375,6 +376,25 @@ const AppLayout = () => {
|
||||
const tokenRef = useRef(token);
|
||||
const refreshPromiseRef = useRef(null);
|
||||
const breadcrumbFetchRef = useRef(new Set());
|
||||
const tagRemovalCursorActiveRef = useRef(false);
|
||||
const setTagRemovalCursor = useCallback((active) => {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (tagRemovalCursorActiveRef.current === active) {
|
||||
return;
|
||||
}
|
||||
const body = document.body;
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
tagRemovalCursorActiveRef.current = active;
|
||||
if (active) {
|
||||
body.classList.add('skeuo-cursor-remove');
|
||||
} else {
|
||||
body.classList.remove('skeuo-cursor-remove');
|
||||
}
|
||||
}, []);
|
||||
const refreshAccessToken = useCallback(async () => {
|
||||
console.log('[Auth] Attempting to refresh access token…');
|
||||
appDispatch({ type: 'TOKEN_REFRESH_START' });
|
||||
@@ -868,6 +888,122 @@ const AppLayout = () => {
|
||||
return map;
|
||||
}, [documents, searchResults]);
|
||||
|
||||
const mapDocumentCaches = useCallback(
|
||||
(mapper) => {
|
||||
if (typeof mapper !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
const applyToList = (list) => {
|
||||
let changed = false;
|
||||
const next = list.map((doc) => {
|
||||
const updated = mapper(doc);
|
||||
if (updated === undefined || updated === doc) {
|
||||
return doc;
|
||||
}
|
||||
changed = true;
|
||||
return updated;
|
||||
});
|
||||
return changed ? next : list;
|
||||
};
|
||||
|
||||
setDocuments((prev) => applyToList(prev));
|
||||
setSearchResults((prev) => {
|
||||
if (!Array.isArray(prev)) {
|
||||
return prev;
|
||||
}
|
||||
return applyToList(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;
|
||||
}
|
||||
let docsChanged = false;
|
||||
const updatedDocs = docs.map((doc) => {
|
||||
const updated = mapper(doc);
|
||||
if (updated === undefined || updated === doc) {
|
||||
return doc;
|
||||
}
|
||||
docsChanged = true;
|
||||
return updated;
|
||||
});
|
||||
if (docsChanged) {
|
||||
changed = true;
|
||||
next.set(key, { ...contents, documents: updatedDocs });
|
||||
} else {
|
||||
next.set(key, contents);
|
||||
}
|
||||
});
|
||||
return changed ? next : prev;
|
||||
});
|
||||
},
|
||||
[setDocuments, setSearchResults, setFolderContents],
|
||||
);
|
||||
|
||||
const updateDocumentCaches = useCallback(
|
||||
(documentId, updater) => {
|
||||
if (!documentId || typeof updater !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
mapDocumentCaches((doc) => {
|
||||
if (!doc || doc.id !== documentId) {
|
||||
return doc;
|
||||
}
|
||||
const updated = updater(doc);
|
||||
return updated === undefined ? doc : updated;
|
||||
});
|
||||
},
|
||||
[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 applySelection = useCallback(
|
||||
(rowKeys, { anchor, interactedKeys = [] } = {}) => {
|
||||
const unique = [];
|
||||
@@ -1488,32 +1624,7 @@ const AppLayout = () => {
|
||||
await api.delete(`/correspondents/${correspondentId}`);
|
||||
await refreshCorrespondents();
|
||||
|
||||
setDocuments((prev) => prev.map((doc) => stripFromDoc(doc)));
|
||||
setSearchResults((prev) =>
|
||||
Array.isArray(prev) ? prev.map((doc) => stripFromDoc(doc)) : prev,
|
||||
);
|
||||
setFolderContents((prev) => {
|
||||
if (!prev.size) {
|
||||
return prev;
|
||||
}
|
||||
let changed = false;
|
||||
const next = new Map();
|
||||
prev.forEach((contents, key) => {
|
||||
if (!Array.isArray(contents?.documents)) {
|
||||
next.set(key, contents);
|
||||
return;
|
||||
}
|
||||
const updatedDocs = contents.documents.map((doc) => stripFromDoc(doc));
|
||||
const mutated = updatedDocs.some((doc, index) => doc !== contents.documents[index]);
|
||||
if (mutated) {
|
||||
changed = true;
|
||||
next.set(key, { ...contents, documents: updatedDocs });
|
||||
} else {
|
||||
next.set(key, contents);
|
||||
}
|
||||
});
|
||||
return changed ? next : prev;
|
||||
});
|
||||
mapDocumentCaches(stripFromDoc);
|
||||
|
||||
setStatusMessage('Correspondent deleted.', 'success');
|
||||
return true;
|
||||
@@ -1523,15 +1634,7 @@ const AppLayout = () => {
|
||||
throw new Error(message);
|
||||
}
|
||||
},
|
||||
[
|
||||
api,
|
||||
refreshCorrespondents,
|
||||
notifyApiError,
|
||||
setStatusMessage,
|
||||
setDocuments,
|
||||
setSearchResults,
|
||||
setFolderContents,
|
||||
],
|
||||
[api, refreshCorrespondents, notifyApiError, setStatusMessage, mapDocumentCaches],
|
||||
);
|
||||
|
||||
async function handleDocumentCorrespondentAttach(
|
||||
@@ -1658,33 +1761,7 @@ const AppLayout = () => {
|
||||
return { ...doc, tags: nextTags };
|
||||
};
|
||||
|
||||
setDocuments((prev) => prev.map((doc) => stripTagFromDoc(doc)));
|
||||
setSearchResults((prev) =>
|
||||
Array.isArray(prev) ? prev.map((doc) => stripTagFromDoc(doc)) : prev,
|
||||
);
|
||||
|
||||
setFolderContents((prev) => {
|
||||
if (!prev.size) {
|
||||
return prev;
|
||||
}
|
||||
let changed = false;
|
||||
const next = new Map();
|
||||
prev.forEach((contents, key) => {
|
||||
if (!Array.isArray(contents?.documents)) {
|
||||
next.set(key, contents);
|
||||
return;
|
||||
}
|
||||
const updatedDocs = contents.documents.map((doc) => stripTagFromDoc(doc));
|
||||
const mutated = updatedDocs.some((doc, index) => doc !== contents.documents[index]);
|
||||
if (mutated) {
|
||||
changed = true;
|
||||
next.set(key, { ...contents, documents: updatedDocs });
|
||||
} else {
|
||||
next.set(key, contents);
|
||||
}
|
||||
});
|
||||
return changed ? next : prev;
|
||||
});
|
||||
mapDocumentCaches(stripTagFromDoc);
|
||||
|
||||
await refreshTags();
|
||||
setStatusMessage('Tag deleted.', 'success');
|
||||
@@ -1700,9 +1777,7 @@ const AppLayout = () => {
|
||||
refreshTags,
|
||||
notifyApiError,
|
||||
setStatusMessage,
|
||||
setDocuments,
|
||||
setSearchResults,
|
||||
setFolderContents,
|
||||
mapDocumentCaches,
|
||||
setActiveTagFilters,
|
||||
],
|
||||
);
|
||||
@@ -3271,56 +3346,11 @@ const AppLayout = () => {
|
||||
const hydratedDetail = assetManager.hydrateDetail(data);
|
||||
const hydratedDocument = hydratedDetail?.document || data.document || data;
|
||||
|
||||
setDocuments((prev) =>
|
||||
prev.map((doc) => {
|
||||
if (doc.id !== documentId) {
|
||||
return doc;
|
||||
}
|
||||
updateDocumentCaches(documentId, (doc) => {
|
||||
if (hydratedDocument) {
|
||||
return { ...doc, ...hydratedDocument };
|
||||
}
|
||||
return { ...doc, title: trimmed };
|
||||
}),
|
||||
);
|
||||
|
||||
setSearchResults((prev) =>
|
||||
prev
|
||||
? prev.map((doc) => {
|
||||
if (doc.id !== documentId) {
|
||||
return doc;
|
||||
}
|
||||
if (hydratedDocument) {
|
||||
return { ...doc, ...hydratedDocument };
|
||||
}
|
||||
return { ...doc, title: trimmed };
|
||||
})
|
||||
: null,
|
||||
);
|
||||
|
||||
setFolderContents((prev) => {
|
||||
let changed = false;
|
||||
const next = new Map();
|
||||
prev.forEach((contents, key) => {
|
||||
if (contents?.documents?.some((doc) => doc.id === documentId)) {
|
||||
changed = true;
|
||||
next.set(key, {
|
||||
...contents,
|
||||
documents: contents.documents.map((doc) =>
|
||||
doc.id === documentId
|
||||
? hydratedDocument
|
||||
? { ...doc, ...hydratedDocument }
|
||||
: { ...doc, title: trimmed }
|
||||
: doc,
|
||||
),
|
||||
});
|
||||
} else {
|
||||
next.set(key, contents);
|
||||
}
|
||||
});
|
||||
if (!changed) {
|
||||
return prev;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
setStatusMessage('Document title updated.', 'success');
|
||||
@@ -3333,20 +3363,52 @@ const AppLayout = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[assetManager, notifyApiError, setStatusMessage, setDocuments, setSearchResults, setFolderContents],
|
||||
[assetManager, notifyApiError, setStatusMessage, updateDocumentCaches],
|
||||
);
|
||||
|
||||
const applyTagRemovalToCaches = useCallback(
|
||||
(documentId, tagId) => {
|
||||
if (!documentId || !tagId) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateDocumentCaches(documentId, (doc) => {
|
||||
if (!Array.isArray(doc.tags)) {
|
||||
return doc;
|
||||
}
|
||||
const nextTags = doc.tags.filter((tag) => tag.id !== tagId);
|
||||
if (nextTags.length === doc.tags.length) {
|
||||
return doc;
|
||||
}
|
||||
return { ...doc, tags: nextTags };
|
||||
});
|
||||
},
|
||||
[updateDocumentCaches],
|
||||
);
|
||||
|
||||
const handleTagRemove = useCallback(
|
||||
async (documentId, tagId) => {
|
||||
async (documentId, tagId, { refreshTagList = true, showMessage = true } = {}) => {
|
||||
if (!documentId || !tagId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.delete(`/documents/${documentId}/tags/${tagId}`);
|
||||
applyTagRemovalToCaches(documentId, tagId);
|
||||
if (refreshTagList) {
|
||||
await refreshTags();
|
||||
}
|
||||
if (showMessage) {
|
||||
setStatusMessage('Tag removed.', 'success');
|
||||
await Promise.all([refreshCurrentFolder(), refreshTags()]);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
notifyApiError(error, 'Failed to remove tag.');
|
||||
const message = error.response?.data?.error || 'Failed to remove tag.';
|
||||
notifyApiError(error, message);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[refreshCurrentFolder, refreshTags, notifyApiError, setStatusMessage],
|
||||
[api, refreshTags, notifyApiError, setStatusMessage, applyTagRemovalToCaches],
|
||||
);
|
||||
|
||||
const handleTagAdd = useCallback(
|
||||
@@ -3392,6 +3454,31 @@ const AppLayout = () => {
|
||||
[api, refreshCurrentFolder, notifyApiError, setStatusMessage],
|
||||
);
|
||||
|
||||
const handleDocumentTagDrop = useCallback(
|
||||
async (documentId, tag) => {
|
||||
if (!documentId || !tag?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tag.sourceDocId && tag.sourceDocId === documentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attached = await handleDocumentTagAttach({ documentId, tagId: tag.id });
|
||||
if (!attached) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tag.sourceDocId && tag.sourceDocId !== documentId) {
|
||||
await handleTagRemove(tag.sourceDocId, tag.id, {
|
||||
refreshTagList: false,
|
||||
showMessage: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
[handleDocumentTagAttach, handleTagRemove],
|
||||
);
|
||||
|
||||
const handleFolderDelete = useCallback(
|
||||
async (folderId) => {
|
||||
if (!token) {
|
||||
@@ -3818,6 +3905,107 @@ const AppLayout = () => {
|
||||
};
|
||||
}, [token, handleFileDrop, currentFolderName, selectedFolder]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
setTagRemovalCursor(false);
|
||||
},
|
||||
[setTagRemovalCursor],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const host = shellRef.current;
|
||||
if (!host) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isTagTransfer = (event) => {
|
||||
const types = event?.dataTransfer?.types;
|
||||
if (!types) {
|
||||
return false;
|
||||
}
|
||||
if (typeof types.includes === 'function') {
|
||||
return TAG_MIME_TYPES.some((type) => types.includes(type));
|
||||
}
|
||||
return TAG_MIME_TYPES.some((type) => Array.from(types).includes(type));
|
||||
};
|
||||
|
||||
const isDocumentRowTarget = (target) =>
|
||||
target instanceof Element ? Boolean(target.closest('tr.document')) : false;
|
||||
|
||||
const handleTagDragOver = (event) => {
|
||||
if (!isTagTransfer(event)) {
|
||||
return;
|
||||
}
|
||||
if (isDocumentRowTarget(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 (isDocumentRowTarget(related)) {
|
||||
setTagRemovalCursor(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
setTagRemovalCursor(false);
|
||||
};
|
||||
|
||||
const handleTagDrop = async (event) => {
|
||||
if (!isTagTransfer(event)) {
|
||||
return;
|
||||
}
|
||||
setTagRemovalCursor(false);
|
||||
if (isDocumentRowTarget(event.target) || event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const raw =
|
||||
event.dataTransfer.getData('application/x-papercrate-tag') ||
|
||||
event.dataTransfer.getData('text/papercrate-tag');
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload = JSON.parse(raw);
|
||||
if (payload?.sourceDocId && payload?.id) {
|
||||
await handleTagRemove(payload.sourceDocId, payload.id, {
|
||||
refreshTagList: false,
|
||||
showMessage: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to remove tag from drop target', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTagDragEnd = () => {
|
||||
setTagRemovalCursor(false);
|
||||
};
|
||||
|
||||
host.addEventListener('dragover', handleTagDragOver, true);
|
||||
host.addEventListener('dragleave', handleTagDragLeave, true);
|
||||
host.addEventListener('drop', handleTagDrop, true);
|
||||
window.addEventListener('dragend', handleTagDragEnd, true);
|
||||
|
||||
return () => {
|
||||
host.removeEventListener('dragover', handleTagDragOver, true);
|
||||
host.removeEventListener('dragleave', handleTagDragLeave, true);
|
||||
host.removeEventListener('drop', handleTagDrop, true);
|
||||
window.removeEventListener('dragend', handleTagDragEnd, true);
|
||||
setTagRemovalCursor(false);
|
||||
};
|
||||
}, [handleTagRemove, setTagRemovalCursor]);
|
||||
|
||||
const handleLogin = useCallback(
|
||||
async (event) => {
|
||||
event.preventDefault();
|
||||
@@ -4053,28 +4241,7 @@ const AppLayout = () => {
|
||||
try {
|
||||
await api.delete(`/documents/${documentId}`);
|
||||
|
||||
setDocuments((prev) => prev.filter((item) => item.id !== documentId));
|
||||
|
||||
setSearchResults((prev) => (prev ? prev.filter((item) => item.id !== documentId) : null));
|
||||
|
||||
setFolderContents((prev) => {
|
||||
let changed = false;
|
||||
const next = new Map();
|
||||
prev.forEach((contents, key) => {
|
||||
if (!contents?.documents) {
|
||||
next.set(key, contents);
|
||||
return;
|
||||
}
|
||||
const filteredDocs = contents.documents.filter((item) => item.id !== documentId);
|
||||
if (filteredDocs.length !== contents.documents.length) {
|
||||
changed = true;
|
||||
next.set(key, { ...contents, documents: filteredDocs });
|
||||
} else {
|
||||
next.set(key, contents);
|
||||
}
|
||||
});
|
||||
return changed ? next : prev;
|
||||
});
|
||||
removeDocumentFromCaches(documentId);
|
||||
|
||||
setPreviewEntries((prev) => {
|
||||
if (!prev.has(documentId)) {
|
||||
@@ -4115,9 +4282,7 @@ const AppLayout = () => {
|
||||
token,
|
||||
documentLookup,
|
||||
setStatusMessage,
|
||||
setDocuments,
|
||||
setSearchResults,
|
||||
setFolderContents,
|
||||
removeDocumentFromCaches,
|
||||
setPreviewEntries,
|
||||
previewInflightRef,
|
||||
previewDocumentId,
|
||||
@@ -4335,6 +4500,7 @@ const AppLayout = () => {
|
||||
? resolveApiPath(doc.current_version.download_path)
|
||||
: null,
|
||||
onTagClick: toggleTagFilter,
|
||||
onDocumentTagDrop: handleDocumentTagDrop,
|
||||
};
|
||||
|
||||
const detailPanelProps = {
|
||||
|
||||
@@ -209,7 +209,9 @@ const Sidebar = ({
|
||||
<span className="meta">{tags.length}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`sidebar-tag-cloud${activeTagSet.size ? ' sidebar-tag-cloud--has-active' : ''}`}
|
||||
className={`sidebar-tag-cloud${
|
||||
activeTagSet.size ? ' sidebar-tag-cloud--has-active' : ''
|
||||
}`}
|
||||
role="list"
|
||||
>
|
||||
{tags.length ? (
|
||||
@@ -226,6 +228,22 @@ const Sidebar = ({
|
||||
style={style || undefined}
|
||||
onClick={() => handleToggleTag(tag.id)}
|
||||
aria-pressed={isActive}
|
||||
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);
|
||||
} catch (
|
||||
// eslint-disable-next-line no-empty
|
||||
error
|
||||
) {}
|
||||
}}
|
||||
>
|
||||
{tag.label}
|
||||
</button>
|
||||
|
||||
@@ -1255,6 +1255,11 @@ button.icon-button.ghost:hover:not([disabled]) {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.documents-panel tbody tr.document.tag-drop-target {
|
||||
background: var(--accent-soft, rgba(63, 106, 216, 0.12));
|
||||
box-shadow: inset 0 0 0 2px var(--accent);
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user