Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43d27cfd27 | ||
|
|
2d2e046c9d | ||
|
|
abee49a335 |
@@ -67,17 +67,21 @@ const DocumentThumbnailImage = ({ document, ensureAssetUrl, getDocumentAsset, al
|
|||||||
[document, ensureAssetUrl, getDocumentAsset],
|
[document, ensureAssetUrl, getDocumentAsset],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (url) {
|
|
||||||
return (
|
return (
|
||||||
|
<div className="document-thumbnail-wrapper">
|
||||||
|
{url ? (
|
||||||
<img
|
<img
|
||||||
src={url}
|
src={url}
|
||||||
alt={alt || ''}
|
alt={alt || ''}
|
||||||
className="document-thumbnail"
|
className="document-thumbnail"
|
||||||
|
draggable={false}
|
||||||
|
onDragStart={(event) => event.preventDefault()}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="thumb-placeholder">DOC</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="thumb-placeholder">DOC</div>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DocumentsTable = ({
|
const DocumentsTable = ({
|
||||||
|
|||||||
+143
-55
@@ -859,9 +859,6 @@ const DetailPanel = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="detail-panel column">
|
<aside className="detail-panel column">
|
||||||
<div className="column-header">
|
|
||||||
<h2>Details</h2>
|
|
||||||
</div>
|
|
||||||
<div className="column-body scrollable">
|
<div className="column-body scrollable">
|
||||||
{selectedCount <= 1 ? renderSingle() : renderBulk()}
|
{selectedCount <= 1 ? renderSingle() : renderBulk()}
|
||||||
</div>
|
</div>
|
||||||
@@ -1876,6 +1873,23 @@ const AppLayout = () => {
|
|||||||
[visibleDocuments],
|
[visibleDocuments],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const documentLookup = useMemo(() => {
|
||||||
|
const map = new Map();
|
||||||
|
const push = (items) => {
|
||||||
|
(items || []).forEach((doc) => {
|
||||||
|
if (doc?.id) {
|
||||||
|
map.set(doc.id, doc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
push(documents);
|
||||||
|
if (Array.isArray(searchResults)) {
|
||||||
|
push(searchResults);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [documents, searchResults]);
|
||||||
|
|
||||||
const applySelection = useCallback(
|
const applySelection = useCallback(
|
||||||
(ids, { anchor, interactedIds = [] } = {}) => {
|
(ids, { anchor, interactedIds = [] } = {}) => {
|
||||||
const visibleSet = new Set(visibleDocumentIds);
|
const visibleSet = new Set(visibleDocumentIds);
|
||||||
@@ -2104,40 +2118,6 @@ const AppLayout = () => {
|
|||||||
}, [focusedRowKey, navigableRowKeys, focusedDocumentId]);
|
}, [focusedRowKey, navigableRowKeys, focusedDocumentId]);
|
||||||
|
|
||||||
|
|
||||||
const handleDocumentDragStart = useCallback(
|
|
||||||
(event, documentId) => {
|
|
||||||
const selection = selectedDocumentIds.includes(documentId)
|
|
||||||
? selectedDocumentIds
|
|
||||||
: [documentId];
|
|
||||||
|
|
||||||
if (!selectedDocumentIds.includes(documentId)) {
|
|
||||||
applySelection([documentId], {
|
|
||||||
anchor: documentId,
|
|
||||||
interactedIds: [documentId],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setDraggedDocumentIds(selection);
|
|
||||||
event.dataTransfer.effectAllowed = 'move';
|
|
||||||
try {
|
|
||||||
event.dataTransfer.setData(
|
|
||||||
'application/x-papercrate-doc-list',
|
|
||||||
JSON.stringify(selection),
|
|
||||||
);
|
|
||||||
} catch (
|
|
||||||
// eslint-disable-next-line no-empty
|
|
||||||
error
|
|
||||||
) {}
|
|
||||||
event.currentTarget.classList.add('dragging');
|
|
||||||
},
|
|
||||||
[selectedDocumentIds, applySelection],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDocumentDragEnd = useCallback((event) => {
|
|
||||||
setDraggedDocumentIds([]);
|
|
||||||
event.currentTarget.classList.remove('dragging');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const ensureFolderData = useCallback(
|
const ensureFolderData = useCallback(
|
||||||
async (folderId, { force = false } = {}) => {
|
async (folderId, { force = false } = {}) => {
|
||||||
if (!force && folderContents.has(folderId)) {
|
if (!force && folderContents.has(folderId)) {
|
||||||
@@ -3263,6 +3243,131 @@ const AppLayout = () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dragPreviewRef = useRef(null);
|
||||||
|
|
||||||
|
const destroyDragPreview = useCallback(() => {
|
||||||
|
const node = dragPreviewRef.current;
|
||||||
|
if (node && node.parentNode) {
|
||||||
|
node.parentNode.removeChild(node);
|
||||||
|
}
|
||||||
|
dragPreviewRef.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => destroyDragPreview, [destroyDragPreview]);
|
||||||
|
|
||||||
|
const createDragPreview = useCallback(
|
||||||
|
(documentsForPreview) => {
|
||||||
|
destroyDragPreview();
|
||||||
|
const docs = (documentsForPreview || []).filter(Boolean);
|
||||||
|
if (!docs.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxVisible = 4;
|
||||||
|
const size = 64;
|
||||||
|
const canvasSize = Math.round(size * 1.6);
|
||||||
|
const visibleDocs = docs.slice(0, maxVisible);
|
||||||
|
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'document-drag-preview';
|
||||||
|
wrapper.style.setProperty('--drag-preview-size', `${canvasSize}px`);
|
||||||
|
|
||||||
|
visibleDocs.forEach((doc, index) => {
|
||||||
|
const layer = document.createElement('div');
|
||||||
|
layer.className = 'document-drag-preview__thumb';
|
||||||
|
const rotationMagnitude = Math.random() * 8 + 2; // 2..10 degrees
|
||||||
|
const rotation = (index % 2 === 0 ? 1 : -1) * rotationMagnitude;
|
||||||
|
layer.style.setProperty('--rotation-deg', `${rotation}deg`);
|
||||||
|
|
||||||
|
const rowEl = doc?.id ? document.getElementById(`document-row-${doc.id}`) : null;
|
||||||
|
const thumbnailEl = rowEl?.querySelector('.document-thumbnail');
|
||||||
|
const placeholderEl = rowEl?.querySelector('.thumb-placeholder');
|
||||||
|
|
||||||
|
let content = null;
|
||||||
|
if (thumbnailEl instanceof HTMLImageElement) {
|
||||||
|
content = thumbnailEl.cloneNode(true);
|
||||||
|
content.draggable = false;
|
||||||
|
content.style.pointerEvents = 'none';
|
||||||
|
} else if (placeholderEl instanceof HTMLElement) {
|
||||||
|
content = placeholderEl.cloneNode(true);
|
||||||
|
content.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
layer.appendChild(content);
|
||||||
|
} else {
|
||||||
|
layer.textContent = 'DOC';
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.appendChild(layer);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (docs.length > 1) {
|
||||||
|
const badge = document.createElement('div');
|
||||||
|
badge.className = 'document-drag-preview__count';
|
||||||
|
badge.textContent = docs.length > maxVisible ? `+${docs.length - maxVisible}` : `${docs.length}`;
|
||||||
|
wrapper.appendChild(badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(wrapper);
|
||||||
|
dragPreviewRef.current = wrapper;
|
||||||
|
return wrapper;
|
||||||
|
},
|
||||||
|
[destroyDragPreview],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDocumentDragStart = useCallback(
|
||||||
|
(event, documentOrId) => {
|
||||||
|
const documentId = typeof documentOrId === 'string' ? documentOrId : documentOrId?.id;
|
||||||
|
if (!documentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAlreadySelected = selectedDocumentIds.includes(documentId);
|
||||||
|
const selection = isAlreadySelected ? [...selectedDocumentIds] : [documentId];
|
||||||
|
|
||||||
|
if (!isAlreadySelected) {
|
||||||
|
applySelection([documentId], {
|
||||||
|
anchor: documentId,
|
||||||
|
interactedIds: [documentId],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewDocs = selection
|
||||||
|
.map((id) => documentLookup.get(id) || null)
|
||||||
|
.filter(Boolean);
|
||||||
|
const previewNode = createDragPreview(previewDocs);
|
||||||
|
|
||||||
|
setDraggedDocumentIds(selection);
|
||||||
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
|
try {
|
||||||
|
event.dataTransfer.setData(
|
||||||
|
'application/x-papercrate-doc-list',
|
||||||
|
JSON.stringify(selection),
|
||||||
|
);
|
||||||
|
} catch (
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
error
|
||||||
|
) {}
|
||||||
|
if (previewNode) {
|
||||||
|
const width = previewNode.offsetWidth || 96;
|
||||||
|
const height = previewNode.offsetHeight || 96;
|
||||||
|
event.dataTransfer.setDragImage(previewNode, width / 2, height / 2);
|
||||||
|
}
|
||||||
|
event.currentTarget.classList.add('dragging');
|
||||||
|
},
|
||||||
|
[selectedDocumentIds, applySelection, documentLookup, createDragPreview],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDocumentDragEnd = useCallback(
|
||||||
|
(event) => {
|
||||||
|
setDraggedDocumentIds([]);
|
||||||
|
event.currentTarget.classList.remove('dragging');
|
||||||
|
destroyDragPreview();
|
||||||
|
},
|
||||||
|
[destroyDragPreview],
|
||||||
|
);
|
||||||
|
|
||||||
const ensurePreviewUrl = useCallback(
|
const ensurePreviewUrl = useCallback(
|
||||||
async (documentId, { force = false } = {}) => {
|
async (documentId, { force = false } = {}) => {
|
||||||
if (!documentId) return null;
|
if (!documentId) return null;
|
||||||
@@ -3509,7 +3614,7 @@ const AppLayout = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
if (unique.length === 1) {
|
if (unique.length === 1) {
|
||||||
await api.patch(`/documents/${unique[0]}/folder`, { folder_id: target });
|
await api.patch(`/documents/${unique[0].id}/folder`, { folder_id: target });
|
||||||
} else {
|
} else {
|
||||||
await api.post('/documents/bulk/move', {
|
await api.post('/documents/bulk/move', {
|
||||||
document_ids: unique,
|
document_ids: unique,
|
||||||
@@ -4597,23 +4702,6 @@ const AppLayout = () => {
|
|||||||
return list.find((doc) => doc.id === focusedDocumentId) || null;
|
return list.find((doc) => doc.id === focusedDocumentId) || null;
|
||||||
}, [searchResults, documents, focusedDocumentId]);
|
}, [searchResults, documents, focusedDocumentId]);
|
||||||
|
|
||||||
const documentLookup = useMemo(() => {
|
|
||||||
const map = new Map();
|
|
||||||
documents.forEach((doc) => {
|
|
||||||
if (doc?.id) {
|
|
||||||
map.set(doc.id, doc);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (Array.isArray(searchResults)) {
|
|
||||||
searchResults.forEach((doc) => {
|
|
||||||
if (doc?.id) {
|
|
||||||
map.set(doc.id, doc);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}, [documents, searchResults]);
|
|
||||||
|
|
||||||
const handleDocumentDelete = useCallback(
|
const handleDocumentDelete = useCallback(
|
||||||
async (documentId) => {
|
async (documentId) => {
|
||||||
if (!documentId) return;
|
if (!documentId) return;
|
||||||
|
|||||||
+82
-7
@@ -313,6 +313,69 @@ button.icon-button.ghost:hover:not([disabled]) {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.document-drag-preview {
|
||||||
|
position: fixed;
|
||||||
|
pointer-events: none;
|
||||||
|
top: -9999px;
|
||||||
|
left: -9999px;
|
||||||
|
width: var(--drag-preview-size, 96px);
|
||||||
|
height: var(--drag-preview-size, 96px);
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-drag-preview__thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.25);
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: rgba(32, 34, 40, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transform: translate(-50%, -50%) rotate(var(--rotation-deg, 0deg));
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-drag-preview__thumb .document-thumbnail,
|
||||||
|
.document-drag-preview__thumb img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-drag-preview__thumb .thumb-placeholder,
|
||||||
|
.document-drag-preview__thumb .thumb-placeholder * {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-drag-preview__count {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4px;
|
||||||
|
right: 4px;
|
||||||
|
background-color: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.25);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.preview-workspace__actions {
|
.preview-workspace__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -856,18 +919,29 @@ button.icon-button.ghost:hover:not([disabled]) {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-thumbnail {
|
.document-thumbnail-wrapper {
|
||||||
width: 36px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
object-fit: cover;
|
background: transparent;
|
||||||
border-radius: 3px;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-thumbnail {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
border-radius: 1px;
|
||||||
box-shadow: 0 1px 3px var(--shadow-medium);
|
box-shadow: 0 1px 3px var(--shadow-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb-placeholder {
|
.thumb-placeholder {
|
||||||
width: 36px;
|
width: 100%;
|
||||||
height: 48px;
|
height: 100%;
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
|
background: var(--surface-subtle);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -875,6 +949,7 @@ button.icon-button.ghost:hover:not([disabled]) {
|
|||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
|
box-shadow: 0 1px 3px var(--shadow-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb-placeholder.icon {
|
.thumb-placeholder.icon {
|
||||||
@@ -1144,7 +1219,7 @@ button.icon-button.ghost:hover:not([disabled]) {
|
|||||||
|
|
||||||
.preview-stack--stacked {
|
.preview-stack--stacked {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 420px;
|
min-height: 320px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user