This commit is contained in:
2025-10-29 18:03:21 +01:00
parent ca30b20873
commit b6bcb72165
6 changed files with 639 additions and 364 deletions
+82 -86
View File
@@ -34,6 +34,84 @@ const DEBUG_DRAG = false;
const DEBUG_FOCUS = true;
const DEBUG_DROP = true;
const signedDistance = (ax, ay, bx, by, px, py) => (bx - ax) * (py - ay) - (by - ay) * (px - ax);
const clipPolygonWithEdge = (subject, edgeStart, edgeEnd) => {
if (!subject.length) {
return [];
}
const result = [];
let prev = subject[subject.length - 1];
let prevInside = signedDistance(edgeStart.x, edgeStart.y, edgeEnd.x, edgeEnd.y, prev.x, prev.y) >= 0;
subject.forEach((curr) => {
const currInside = signedDistance(edgeStart.x, edgeStart.y, edgeEnd.x, edgeEnd.y, curr.x, curr.y) >= 0;
if (currInside !== prevInside) {
const dx = curr.x - prev.x;
const dy = curr.y - prev.y;
const denom = (edgeEnd.x - edgeStart.x) * dy - (edgeEnd.y - edgeStart.y) * dx;
if (Math.abs(denom) > 1e-9) {
const t = ((edgeStart.x - prev.x) * dy - (edgeStart.y - prev.y) * dx) / denom;
result.push({
x: edgeStart.x + t * (edgeEnd.x - edgeStart.x),
y: edgeStart.y + t * (edgeEnd.y - edgeStart.y),
});
}
}
if (currInside) {
result.push(curr);
}
prev = curr;
prevInside = currInside;
});
return result;
};
const clipPolygon = (subject, clipShape) => {
if (!subject.length) {
return [];
}
let output = subject;
let prev = clipShape[clipShape.length - 1];
for (let index = 0; index < clipShape.length; index += 1) {
const curr = clipShape[index];
output = clipPolygonWithEdge(output, prev, curr);
if (!output.length) {
return [];
}
prev = curr;
}
return output;
};
const isPointInsideConvex = (point, polygon) => {
if (!polygon.length) {
return false;
}
let prev = polygon[polygon.length - 1];
for (let index = 0; index < polygon.length; index += 1) {
const curr = polygon[index];
if (signedDistance(prev.x, prev.y, curr.x, curr.y, point.x, point.y) < -1e-6) {
return false;
}
prev = curr;
}
return true;
};
const polygonCentroid = (polygon) => {
let x = 0;
let y = 0;
polygon.forEach((point) => {
x += point.x;
y += point.y;
});
const count = polygon.length || 1;
return {
x: x / count,
y: y / count,
};
};
const readTransferData = (dataTransfer, mimeTypes) => {
if (!dataTransfer) {
return null;
@@ -866,84 +944,6 @@ const DesktopWorkspace = ({
},
[resolvePreviewDimensions],
);
const signedDistance = (ax, ay, bx, by, px, py) => (bx - ax) * (py - ay) - (by - ay) * (px - ax);
const clipPolygonWithEdge = (subject, edgeStart, edgeEnd) => {
if (!subject.length) {
return [];
}
const result = [];
let prev = subject[subject.length - 1];
let prevInside = signedDistance(edgeStart.x, edgeStart.y, edgeEnd.x, edgeEnd.y, prev.x, prev.y) >= 0;
subject.forEach((curr) => {
const currInside = signedDistance(edgeStart.x, edgeStart.y, edgeEnd.x, edgeEnd.y, curr.x, curr.y) >= 0;
if (currInside !== prevInside) {
const dx = curr.x - prev.x;
const dy = curr.y - prev.y;
const denom = (edgeEnd.x - edgeStart.x) * dy - (edgeEnd.y - edgeStart.y) * dx;
if (Math.abs(denom) > 1e-9) {
const t = ((edgeStart.x - prev.x) * dy - (edgeStart.y - prev.y) * dx) / denom;
result.push({
x: edgeStart.x + t * (edgeEnd.x - edgeStart.x),
y: edgeStart.y + t * (edgeEnd.y - edgeStart.y),
});
}
}
if (currInside) {
result.push(curr);
}
prev = curr;
prevInside = currInside;
});
return result;
};
const clipPolygon = (subject, clipShape) => {
if (!subject.length) {
return [];
}
let output = subject;
let prev = clipShape[clipShape.length - 1];
for (let index = 0; index < clipShape.length; index += 1) {
const curr = clipShape[index];
output = clipPolygonWithEdge(output, prev, curr);
if (!output.length) {
return [];
}
prev = curr;
}
return output;
};
const isPointInsideConvex = (point, polygon) => {
if (!polygon.length) {
return false;
}
let prev = polygon[polygon.length - 1];
for (let index = 0; index < polygon.length; index += 1) {
const curr = polygon[index];
if (signedDistance(prev.x, prev.y, curr.x, curr.y, point.x, point.y) < -1e-6) {
return false;
}
prev = curr;
}
return true;
};
const polygonCentroid = (polygon) => {
let x = 0;
let y = 0;
polygon.forEach((point) => {
x += point.x;
y += point.y;
});
const count = polygon.length || 1;
return {
x: x / count,
y: y / count,
};
};
const recalcVisibleDocIds = useCallback(() => {
const layoutMap = layoutRef.current;
const canvasWidth = canvasSize.width || DEFAULT_CANVAS_WIDTH;
@@ -1069,7 +1069,6 @@ const recalcVisibleDocIds = useCallback(() => {
canvasSize.height,
ensureDocumentSize,
documentLookup,
docSizeVersion,
]);
const syncLayoutSnapshot = useCallback(() => {
@@ -1080,7 +1079,8 @@ const syncLayoutSnapshot = useCallback(() => {
const container = containerRef.current;
if (!container) return () => {};
if (process.env.NODE_ENV !== 'production') {
const nodeEnv = typeof globalThis !== 'undefined' ? globalThis.process?.env?.NODE_ENV : undefined;
if (nodeEnv !== 'production') {
console.log('[skeuo] canvas element', container);
}
@@ -1207,7 +1207,7 @@ const syncLayoutSnapshot = useCallback(() => {
useEffect(() => {
recalcVisibleDocIds();
}, [recalcVisibleDocIds, items.length, canvasSize.width, canvasSize.height]);
}, [recalcVisibleDocIds, items.length, canvasSize.width, canvasSize.height, docSizeVersion]);
useEffect(() => {
if (draggingId && !items.some((doc) => doc.id === draggingId)) {
@@ -1849,16 +1849,12 @@ const DesktopWorkspaceView = () => {
closeOverlay,
overlayOriginRect,
overlayOriginTransform,
docSizeVersion,
} = useDesktopContext();
const { handlePointerDown, handlePointerMove, handlePointerUp, handlePointerCancel } =
useDocumentDrag();
const allSizesReady = useMemo(
() => items.every((doc) => ensureDocumentSize(doc)),
[items, ensureDocumentSize, docSizeVersion],
);
const allSizesReady = items.every((doc) => ensureDocumentSize(doc));
return (
<>
+1
View File
@@ -196,6 +196,7 @@ const useDocumentDrag = () => {
recalcVisibleDocIds();
},
[
bringToFront,
canvasPadding,
canvasSize.height,
canvasSize.width,
+2
View File
@@ -828,11 +828,13 @@ const DetailPanel = ({
warmNavigator(stackPreviewNavigator);
}, [
ensureAssetUrl,
singlePreviewNavigator,
singlePreviewNavigator.documentId,
singlePreviewNavigator.asset,
singlePreviewNavigator.ordinal,
singlePreviewNavigator.canGoPrev,
singlePreviewNavigator.canGoNext,
stackPreviewNavigator,
stackPreviewNavigator.documentId,
stackPreviewNavigator.asset,
stackPreviewNavigator.ordinal,
+1 -1
View File
@@ -172,7 +172,7 @@ const DocumentThumbnailImage = ({
} else {
delete node.dataset.thumbnailAspect;
}
}, [aspectRatio]);
}, [aspectRatio, visibilityRef]);
return (
<div className="document-thumbnail-wrapper" ref={visibilityRef}>
+408 -132
View File
@@ -291,6 +291,90 @@ const resolveFolderRowKey = (folderId) =>
const hasFiles = (event) =>
Array.from(event.dataTransfer?.types || []).includes('Files');
const isAssetEquivalent = (lhs, rhs) => {
if (!lhs || !rhs) return false;
const lhsView = createAssetView(lhs);
const rhsView = createAssetView(rhs);
const lhsPrimaryMetadata = lhsView.getPrimaryMetadata() || lhs?.metadata;
const rhsPrimaryMetadata = rhsView.getPrimaryMetadata() || rhs?.metadata;
const lhsCardinality = lhsView.getCardinality() || lhs?.cardinality || null;
const rhsCardinality = rhsView.getCardinality() || rhs?.cardinality || null;
const lhsObjects = lhsView.getObjects();
const rhsObjects = rhsView.getObjects();
const objectsComparable =
lhsObjects.length === rhsObjects.length
&& lhsObjects.every((entry, index) => {
const other = rhsObjects[index];
if (!other) return false;
if (entry.ordinal !== other.ordinal) return false;
if (entry.url && other.url && entry.url === other.url) {
return true;
}
if (!entry.url && !other.url) {
return JSON.stringify(entry.metadata || null) === JSON.stringify(other.metadata || null);
}
return entry.url === other.url;
});
return (
lhs.id === rhs.id
&& lhs.url === rhs.url
&& lhsPrimaryMetadata?.width === rhsPrimaryMetadata?.width
&& lhsPrimaryMetadata?.height === rhsPrimaryMetadata?.height
&& lhs.mime_type === rhs.mime_type
&& lhs.asset_type === rhs.asset_type
&& lhs.created_at === rhs.created_at
&& lhsCardinality === rhsCardinality
&& objectsComparable
);
};
const mergeAssetIntoGroup = (group, assetData) => {
if (!assetData || !assetData.asset_type) {
if (Array.isArray(group)) {
return group;
}
return group || {};
}
if (Array.isArray(group) || !group) {
const list = Array.isArray(group) ? group : [];
const index = list.findIndex((item) => item?.id === assetData.id);
if (index >= 0) {
const existing = list[index];
if (isAssetEquivalent(existing, assetData)) {
return list;
}
const next = list.slice();
next[index] = { ...existing, ...assetData };
return next;
}
return list.concat({ ...assetData });
}
const key = assetData.asset_type;
const previous = group?.[key];
if (previous && isAssetEquivalent(previous, assetData)) {
return group;
}
const next = { ...(group || {}) };
next[key] = { ...(previous || {}), ...assetData };
return next;
};
const mergeAssetIntoDocument = (doc, assetData) => {
if (!doc) return doc;
const existingGroup = doc.current_version?.assets || null;
const nextGroup = mergeAssetIntoGroup(existingGroup, assetData);
if (nextGroup === existingGroup) {
return doc;
}
const updatedCurrentVersion = doc.current_version
? { ...doc.current_version, assets: nextGroup }
: { assets: nextGroup };
return { ...doc, current_version: updatedCurrentVersion };
};
const createRootNode = () => ({
id: 'root',
name: DEFAULT_FOLDER_NAME,
@@ -591,89 +675,6 @@ const AppLayout = () => {
return getAssetFromVersion(doc.current_version || null, type);
}, []);
const isAssetEquivalent = (lhs, rhs) => {
if (!lhs || !rhs) return false;
const lhsView = createAssetView(lhs);
const rhsView = createAssetView(rhs);
const lhsPrimaryMetadata = lhsView.getPrimaryMetadata() || lhs?.metadata;
const rhsPrimaryMetadata = rhsView.getPrimaryMetadata() || rhs?.metadata;
const lhsCardinality = lhsView.getCardinality() || lhs?.cardinality || null;
const rhsCardinality = rhsView.getCardinality() || rhs?.cardinality || null;
const lhsObjects = lhsView.getObjects();
const rhsObjects = rhsView.getObjects();
const objectsComparable = lhsObjects.length === rhsObjects.length
&& lhsObjects.every((entry, index) => {
const other = rhsObjects[index];
if (!other) return false;
if (entry.ordinal !== other.ordinal) return false;
if (entry.url && other.url && entry.url === other.url) {
return true;
}
if (!entry.url && !other.url) {
return JSON.stringify(entry.metadata || null) === JSON.stringify(other.metadata || null);
}
return entry.url === other.url;
});
return (
lhs.id === rhs.id &&
lhs.url === rhs.url &&
lhsPrimaryMetadata?.width === rhsPrimaryMetadata?.width &&
lhsPrimaryMetadata?.height === rhsPrimaryMetadata?.height &&
lhs.mime_type === rhs.mime_type &&
lhs.asset_type === rhs.asset_type &&
lhs.created_at === rhs.created_at &&
lhsCardinality === rhsCardinality &&
objectsComparable
);
};
const mergeAssetIntoGroup = (group, assetData) => {
if (!assetData || !assetData.asset_type) {
if (Array.isArray(group)) {
return group;
}
return group || {};
}
if (Array.isArray(group) || !group) {
const list = Array.isArray(group) ? group : [];
const index = list.findIndex((item) => item?.id === assetData.id);
if (index >= 0) {
const existing = list[index];
if (isAssetEquivalent(existing, assetData)) {
return list;
}
const next = list.slice();
next[index] = { ...existing, ...assetData };
return next;
}
return list.concat({ ...assetData });
}
const key = assetData.asset_type;
const previous = group?.[key];
if (previous && isAssetEquivalent(previous, assetData)) {
return group;
}
const next = { ...(group || {}) };
next[key] = { ...(previous || {}), ...assetData };
return next;
};
const mergeAssetIntoDocument = (doc, assetData) => {
if (!doc) return doc;
const existingGroup = doc.current_version?.assets || null;
const nextGroup = mergeAssetIntoGroup(existingGroup, assetData);
if (nextGroup === existingGroup) {
return doc;
}
const updatedCurrentVersion = doc.current_version
? { ...doc.current_version, assets: nextGroup }
: { assets: nextGroup };
return { ...doc, current_version: updatedCurrentVersion };
};
const bootstrapInitializedRef = useRef(false);
const selectionInitializedRef = useRef(false);
const dragCounterRef = useRef(0);
@@ -1557,7 +1558,7 @@ const AppLayout = () => {
return enriched;
},
[api, assetManager, folderContents],
[assetManager, folderContents],
);
const isInvalidFolderDrop = useCallback(
@@ -1681,7 +1682,15 @@ const AppLayout = () => {
}
}
},
[api, folderNodes, ensureFolderData, selectedFolder, setSelectedFolder, setFolderNodes, notifyApiError],
[
folderNodes,
ensureFolderData,
selectedFolder,
setSelectedFolder,
setFolderNodes,
notifyApiError,
setStatusMessage,
],
);
const refreshTags = useCallback(async () => {
@@ -1698,7 +1707,7 @@ const AppLayout = () => {
}
notifyApiError(error, 'Unable to load tags.');
}
}, [api, notifyApiError]);
}, [notifyApiError]);
const refreshCorrespondents = useCallback(async () => {
const requestTenantId = tenantIdRef.current;
@@ -1714,7 +1723,7 @@ const AppLayout = () => {
}
notifyApiError(error, 'Unable to load correspondents.');
}
}, [api, notifyApiError]);
}, [notifyApiError]);
const refreshWebdavTokens = useCallback(async () => {
if (!token) {
@@ -1729,7 +1738,7 @@ const AppLayout = () => {
} finally {
setWebdavTokensLoading(false);
}
}, [api, notifyApiError, token]);
}, [notifyApiError, token]);
const createWebdavToken = useCallback(
async ({ label, expires_at } = {}) => {
@@ -1766,7 +1775,7 @@ const AppLayout = () => {
setCreatingWebdavToken(false);
}
},
[api, creatingWebdavToken, notifyApiError, refreshWebdavTokens, setStatusMessage],
[creatingWebdavToken, notifyApiError, refreshWebdavTokens, setStatusMessage],
);
const deleteWebdavToken = useCallback(
@@ -1787,7 +1796,7 @@ const AppLayout = () => {
setDeletingWebdavTokenId(null);
}
},
[api, refreshWebdavTokens, notifyApiError, setStatusMessage],
[refreshWebdavTokens, notifyApiError, setStatusMessage],
);
const dismissCreatedWebdavToken = useCallback(() => {
@@ -1836,7 +1845,7 @@ const AppLayout = () => {
throw new Error(message);
}
},
[api, refreshTags, notifyApiError, setStatusMessage],
[refreshTags, notifyApiError, setStatusMessage],
);
const handleTagCreate = useCallback(
@@ -1852,7 +1861,7 @@ const AppLayout = () => {
throw new Error(message);
}
},
[api, refreshTags, notifyApiError, setStatusMessage, tagManager],
[refreshTags, notifyApiError, setStatusMessage, tagManager],
);
const handleCorrespondentUpdate = useCallback(
@@ -1885,7 +1894,7 @@ const AppLayout = () => {
throw new Error(message);
}
},
[api, refreshCorrespondents, notifyApiError, setStatusMessage],
[refreshCorrespondents, notifyApiError, setStatusMessage],
);
const handleCorrespondentCreate = useCallback(
@@ -1905,7 +1914,7 @@ const AppLayout = () => {
throw new Error(message);
}
},
[api, refreshCorrespondents, notifyApiError, setStatusMessage],
[refreshCorrespondents, notifyApiError, setStatusMessage],
);
const handleCorrespondentDelete = useCallback(
@@ -1939,13 +1948,11 @@ const AppLayout = () => {
throw new Error(message);
}
},
[api, refreshCorrespondents, notifyApiError, setStatusMessage, mapDocumentCaches],
[refreshCorrespondents, notifyApiError, setStatusMessage, mapDocumentCaches],
);
async function handleDocumentCorrespondentAttach(
{ documentId, correspondentId },
{ notify = true, refresh = true } = {},
) {
const handleDocumentCorrespondentAttach = useCallback(
async ({ documentId, correspondentId }, { notify = true, refresh = true } = {}) => {
if (!documentId || !correspondentId) {
throw new Error('Missing document or correspondent.');
}
@@ -1966,12 +1973,12 @@ const AppLayout = () => {
notifyApiError(error, message);
throw new Error(message);
}
}
},
[notifyApiError, refreshCurrentFolder, setStatusMessage],
);
async function handleCorrespondentRemove(
{ documentId, correspondentId },
{ notify = true, refresh = true } = {},
) {
const handleCorrespondentRemove = useCallback(
async ({ documentId, correspondentId }, { notify = true, refresh = true } = {}) => {
if (!documentId || !correspondentId) {
throw new Error('Missing document or correspondent.');
}
@@ -1989,7 +1996,9 @@ const AppLayout = () => {
notifyApiError(error, message);
throw new Error(message);
}
}
},
[notifyApiError, refreshCurrentFolder, setStatusMessage],
);
const handleCorrespondentAdd = useCallback(
async ({ document, name, input }) => {
@@ -2070,7 +2079,6 @@ const AppLayout = () => {
}
},
[
api,
refreshTags,
notifyApiError,
setStatusMessage,
@@ -2367,7 +2375,6 @@ const AppLayout = () => {
[
correspondentLookupByName,
handleCorrespondentCreate,
api,
refreshCurrentFolder,
resolveTargetDocumentIds,
setStatusMessage,
@@ -2414,7 +2421,7 @@ const AppLayout = () => {
setStatusMessage('No correspondents changed.', 'info');
}
},
[api, refreshCurrentFolder, resolveTargetDocumentIds, setStatusMessage],
[refreshCurrentFolder, resolveTargetDocumentIds, setStatusMessage],
);
useEffect(() => {
@@ -2528,7 +2535,6 @@ const AppLayout = () => {
[
resolveTargetDocumentIds,
tags,
api,
refreshTags,
refreshCurrentFolder,
notifyApiError,
@@ -2629,7 +2635,7 @@ const AppLayout = () => {
setLoading(false);
}
},
[resolveTargetDocumentIds, api, notifyApiError, setStatusMessage],
[resolveTargetDocumentIds, notifyApiError, setStatusMessage],
);
const uploadFile = useCallback(
@@ -2727,12 +2733,7 @@ const AppLayout = () => {
throw error;
}
},
[
assetManager,
setDocuments,
setSearchResults,
notifyApiError,
],
[assetManager, setDocuments, setSearchResults, notifyApiError],
);
const dragPreviewRef = useRef(null);
@@ -3082,7 +3083,7 @@ const AppLayout = () => {
previewInflightRef.current.set(documentId, request);
return request;
},
[previewEntries, notifyApiError, resolveApiPath],
[previewEntries, notifyApiError],
);
const extractFilesFromDataTransfer = useCallback(async (dataTransfer) => {
@@ -3327,7 +3328,6 @@ const AppLayout = () => {
}
},
[
api,
ensureFolderData,
refreshCurrentFolder,
selectedFolder,
@@ -3402,7 +3402,6 @@ const AppLayout = () => {
[
searchResults,
documents,
api,
assetManager,
setDocuments,
selectedFolder,
@@ -3665,7 +3664,7 @@ const AppLayout = () => {
return false;
}
},
[api, refreshTags, notifyApiError, setStatusMessage, applyTagRemovalToCaches],
[refreshTags, notifyApiError, setStatusMessage, applyTagRemovalToCaches],
);
const handleTagAdd = useCallback(
@@ -3734,7 +3733,6 @@ const AppLayout = () => {
}
},
[
api,
refreshCurrentFolder,
notifyApiError,
setStatusMessage,
@@ -3899,7 +3897,7 @@ const AppLayout = () => {
setLoading(false);
}
},
[api, token, notifyApiError, setStatusMessage],
[token, notifyApiError, setStatusMessage],
);
const handleFolderCreate = useCallback(
@@ -4379,7 +4377,8 @@ const AppLayout = () => {
}
}, [appDispatch, setStatusMessage]);
const folderClickHandlers = {
const folderClickHandlers = useMemo(
() => ({
onToggle: async (folderId) => {
const node = folderNodes.get(folderId);
const nextExpanded = !(node?.expanded ?? false);
@@ -4533,6 +4532,177 @@ const AppLayout = () => {
onDragLeave: (event) => {
event.currentTarget.classList.remove('is-drop-target');
},
}),
[
draggedDocumentIds,
draggedFolderId,
ensureFolderData,
folderNodes,
handleFileDrop,
isInvalidFolderDrop,
moveDocumentsToFolder,
moveFolder,
notifyApiError,
selectFolder,
selectedFolder,
setDraggedDocumentIds,
setDraggedFolderId,
setFolderNodes,
setStatusMessage,
],
);
const node = folderNodes.get(folderId);
const nextExpanded = !(node?.expanded ?? false);
if (nextExpanded) {
try {
await ensureFolderData(folderId, {
includeDocuments: false,
prefetchDepth: 1,
});
} catch (error) {
notifyApiError(error, 'Failed to load folder.');
}
} else if (node && !node.loaded) {
try {
await ensureFolderData(folderId, {
includeDocuments: false,
prefetchDepth: 1,
});
} catch (error) {
notifyApiError(error, 'Failed to load folder.');
}
}
setFolderNodes((prev) => {
const next = new Map(prev);
const current = next.get(folderId);
if (!current) return prev;
next.set(folderId, { ...current, expanded: nextExpanded });
return next;
});
},
onSelect: selectFolder,
onDrop: async (event, folderId) => {
event.preventDefault();
event.stopPropagation();
event.currentTarget.classList.remove('is-drop-target');
let folderIds = [];
try {
const rawFolderList = event.dataTransfer.getData('application/x-papercrate-folder-list');
if (rawFolderList) {
const parsed = JSON.parse(rawFolderList);
if (Array.isArray(parsed)) {
folderIds = parsed.filter(Boolean);
}
}
} catch (
// eslint-disable-next-line no-empty
error
) {}
if (!folderIds.length) {
let folderSourceId = draggedFolderId;
if (!folderSourceId) {
try {
if (event.dataTransfer.types?.includes('application/x-papercrate-folder')) {
folderSourceId = event.dataTransfer.getData('application/x-papercrate-folder');
}
} catch (
// eslint-disable-next-line no-empty
error
) {}
}
if (folderSourceId) {
folderIds = [folderSourceId];
}
}
folderIds = Array.from(new Set(folderIds.filter(Boolean)));
if (folderIds.length) {
setDraggedFolderId(null);
const invalidMove = folderIds.some((sourceId) => isInvalidFolderDrop(sourceId, folderId));
if (invalidMove) {
setStatusMessage(
'Cannot move a folder into itself or one of its descendants.',
'error',
);
return;
}
for (const sourceId of folderIds) {
// eslint-disable-next-line no-await-in-loop
await moveFolder(sourceId, folderId);
}
}
if (hasFiles(event)) {
await handleFileDrop(event.dataTransfer, folderId);
return;
}
let docIds = [];
try {
const raw = event.dataTransfer.getData('application/x-papercrate-doc-list');
if (raw) {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
docIds = parsed.filter(Boolean);
}
}
} catch (
// eslint-disable-next-line no-empty
error
) {}
if (!docIds.length) {
try {
const single = event.dataTransfer.getData('application/x-papercrate-doc');
if (single) {
docIds = [single];
}
} catch (
// eslint-disable-next-line no-empty
error
) {}
}
if (!docIds.length && draggedDocumentIds.length) {
docIds = draggedDocumentIds;
}
docIds = Array.from(new Set(docIds));
if (!docIds.length || folderId === selectedFolder) {
return;
}
setDraggedDocumentIds([]);
await moveDocumentsToFolder(docIds, folderId);
},
onDragOver: (event, folderId) => {
const folderDragActive = Boolean(draggedFolderId);
if (folderDragActive && isInvalidFolderDrop(draggedFolderId, folderId)) {
return;
}
if (hasFiles(event)) {
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
event.currentTarget.classList.add('is-drop-target');
return;
}
if (draggedDocumentIds.length || folderDragActive) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
event.currentTarget.classList.add('is-drop-target');
}
},
onDragLeave: (event) => {
event.currentTarget.classList.remove('is-drop-target');
},
};
const selectedDocument = useMemo(() => {
@@ -4600,7 +4770,6 @@ const AppLayout = () => {
}
},
[
api,
token,
documentLookup,
setStatusMessage,
@@ -5016,7 +5185,8 @@ const AppLayout = () => {
],
);
const documentsTableProps = {
const documentsTableProps = useMemo(
() => ({
currentFolderName,
breadcrumbs,
onRefresh: refreshCurrentFolder,
@@ -5066,9 +5236,56 @@ const AppLayout = () => {
viewMode: documentsViewMode,
onViewModeChange: handleDocumentsViewModeChange,
onClearSelection: clearDocumentSelection,
};
}),
[
activeCorrespondentFilters,
breadcrumbs,
clearDocumentSelection,
currentFolderName,
currentSubfolders,
documents,
documentsViewMode,
draggedDocumentIds,
draggedFolderId,
focusedDocumentId,
focusedRowKey,
folderClickHandlers,
handleDocumentDelete,
handleDocumentDragEnd,
handleDocumentDragStart,
handleDocumentListFocus,
handleDocumentListKeyDown,
handleDocumentRowClick,
handleDocumentTagDrop,
handleDocumentTitleUpdate,
handleDocumentsViewModeChange,
handleFolderDelete,
handleFolderDragEnd,
handleFolderDragStart,
handleFolderRename,
handleFolderRowClick,
isFilterActive,
creatingFolder,
openCreateFolderModal,
openDocumentPreview,
refreshCurrentFolder,
searchLoading,
searchResults,
selectFolder,
selectedDocumentIds,
selectedFolderIds,
setFocusedRowKey,
showSkeuoWorkspace,
tagLookupById,
toggleCorrespondentFilter,
toggleTagFilter,
ensureAssetUrl,
getDocumentAsset,
],
);
const sidebarProps = {
const sidebarProps = useMemo(
() => ({
folderNodes,
onToggle: folderClickHandlers.onToggle,
onSelect: folderClickHandlers.onSelect,
@@ -5102,7 +5319,39 @@ const AppLayout = () => {
activeTenantId: currentTenantId,
onSelectTenant: handleTenantSelect,
onOpenSettings: openSettingsModal,
};
}),
[
activeCorrespondentFilters,
activeTagFilters,
appStatus,
clearFilters,
correspondents,
currentTenantId,
folderClickHandlers,
folderNodes,
handleFolderDelete,
handleFolderDragEnd,
handleFolderDragStart,
handleFolderRename,
handleLogout,
handleSearchChange,
handleSearchSubmit,
handleTenantSelect,
loading,
openSettingsModal,
previewActive,
searchQuery,
draggedFolderId,
isFilterActive,
selectedFolder,
status,
tags,
tenantOptions,
tenantSlug,
toggleCorrespondentFilter,
toggleTagFilter,
],
);
const handleDetailPanelClose = useCallback(() => {
setDetailPanelOpen(false);
@@ -5111,7 +5360,8 @@ const AppLayout = () => {
setDetailPanelDocs([]);
}, [clearDocumentSelection]);
const detailPanelProps = {
const detailPanelProps = useMemo(
() => ({
selectedDocuments: detailPanelSelectedDocuments,
tags,
tagLookupById,
@@ -5138,7 +5388,35 @@ const AppLayout = () => {
onFolderNavigate: selectFolder,
onClose: handleDetailPanelClose,
resolveFolderPath,
};
}),
[
activePreviewId,
correspondents,
detailPanelSelectedDocuments,
ensureAssetUrl,
ensurePreviewData,
getDocumentAsset,
handleBulkCorrespondentAdd,
handleBulkCorrespondentRemove,
handleBulkSelectionReanalyze,
handleBulkTagAddFromDetail,
handleBulkTagRemoveFromDetail,
handleCorrespondentAdd,
handleCorrespondentRemove,
handleDetailPanelClose,
handleDocumentTitleUpdate,
handleTagAdd,
handleTagRemove,
handleThumbnailRegeneration,
openDocumentPreview,
promoteSelectionOrder,
resolveFolderPath,
selectFolder,
selectedPreviewEntry,
tags,
tagLookupById,
],
);
const skeuoWorkspaceProps = useMemo(
() => ({
@@ -5210,7 +5488,6 @@ const AppLayout = () => {
ensureAssetUrl,
getDocumentAsset,
notifyApiError,
resolveApiPath,
openTagsModal,
openCorrespondentsModal,
openSettingsModal,
@@ -5253,7 +5530,6 @@ const AppLayout = () => {
ensureAssetUrl,
getDocumentAsset,
notifyApiError,
resolveApiPath,
openTagsModal,
openCorrespondentsModal,
openSettingsModal,
+3 -3
View File
@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React from 'react';
import { formatFileSize } from '../utils/format';
import { DownloadIcon, TextScanIcon, AnalyzeIcon, CloseIcon } from '../ui/icons';
import { openOcrTextInNewTab } from '../utils/ocr';
@@ -24,7 +24,7 @@ const PreviewWorkspace = ({
const tags = Array.isArray(document.tags) ? document.tags : [];
const correspondents = Array.isArray(document.correspondents) ? document.correspondents : [];
const metadataSummary = useMemo(() => {
const metadataSummary = (() => {
const rows = [];
if (mime) rows.push(['Type', mime]);
if (sizeLabel) rows.push(['Size', sizeLabel]);
@@ -45,7 +45,7 @@ const PreviewWorkspace = ({
]);
}
return rows;
}, [mime, sizeLabel, issuedAt, createdAt, updatedAt, folderName, tags, correspondents]);
})();
return (
<section className="preview-workspace">