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_FOCUS = true;
const DEBUG_DROP = 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) => { const readTransferData = (dataTransfer, mimeTypes) => {
if (!dataTransfer) { if (!dataTransfer) {
return null; return null;
@@ -866,84 +944,6 @@ const DesktopWorkspace = ({
}, },
[resolvePreviewDimensions], [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 recalcVisibleDocIds = useCallback(() => {
const layoutMap = layoutRef.current; const layoutMap = layoutRef.current;
const canvasWidth = canvasSize.width || DEFAULT_CANVAS_WIDTH; const canvasWidth = canvasSize.width || DEFAULT_CANVAS_WIDTH;
@@ -1069,7 +1069,6 @@ const recalcVisibleDocIds = useCallback(() => {
canvasSize.height, canvasSize.height,
ensureDocumentSize, ensureDocumentSize,
documentLookup, documentLookup,
docSizeVersion,
]); ]);
const syncLayoutSnapshot = useCallback(() => { const syncLayoutSnapshot = useCallback(() => {
@@ -1080,7 +1079,8 @@ const syncLayoutSnapshot = useCallback(() => {
const container = containerRef.current; const container = containerRef.current;
if (!container) return () => {}; 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); console.log('[skeuo] canvas element', container);
} }
@@ -1207,7 +1207,7 @@ const syncLayoutSnapshot = useCallback(() => {
useEffect(() => { useEffect(() => {
recalcVisibleDocIds(); recalcVisibleDocIds();
}, [recalcVisibleDocIds, items.length, canvasSize.width, canvasSize.height]); }, [recalcVisibleDocIds, items.length, canvasSize.width, canvasSize.height, docSizeVersion]);
useEffect(() => { useEffect(() => {
if (draggingId && !items.some((doc) => doc.id === draggingId)) { if (draggingId && !items.some((doc) => doc.id === draggingId)) {
@@ -1849,16 +1849,12 @@ const DesktopWorkspaceView = () => {
closeOverlay, closeOverlay,
overlayOriginRect, overlayOriginRect,
overlayOriginTransform, overlayOriginTransform,
docSizeVersion,
} = useDesktopContext(); } = useDesktopContext();
const { handlePointerDown, handlePointerMove, handlePointerUp, handlePointerCancel } = const { handlePointerDown, handlePointerMove, handlePointerUp, handlePointerCancel } =
useDocumentDrag(); useDocumentDrag();
const allSizesReady = useMemo( const allSizesReady = items.every((doc) => ensureDocumentSize(doc));
() => items.every((doc) => ensureDocumentSize(doc)),
[items, ensureDocumentSize, docSizeVersion],
);
return ( return (
<> <>
+1
View File
@@ -196,6 +196,7 @@ const useDocumentDrag = () => {
recalcVisibleDocIds(); recalcVisibleDocIds();
}, },
[ [
bringToFront,
canvasPadding, canvasPadding,
canvasSize.height, canvasSize.height,
canvasSize.width, canvasSize.width,
+2
View File
@@ -828,11 +828,13 @@ const DetailPanel = ({
warmNavigator(stackPreviewNavigator); warmNavigator(stackPreviewNavigator);
}, [ }, [
ensureAssetUrl, ensureAssetUrl,
singlePreviewNavigator,
singlePreviewNavigator.documentId, singlePreviewNavigator.documentId,
singlePreviewNavigator.asset, singlePreviewNavigator.asset,
singlePreviewNavigator.ordinal, singlePreviewNavigator.ordinal,
singlePreviewNavigator.canGoPrev, singlePreviewNavigator.canGoPrev,
singlePreviewNavigator.canGoNext, singlePreviewNavigator.canGoNext,
stackPreviewNavigator,
stackPreviewNavigator.documentId, stackPreviewNavigator.documentId,
stackPreviewNavigator.asset, stackPreviewNavigator.asset,
stackPreviewNavigator.ordinal, stackPreviewNavigator.ordinal,
+1 -1
View File
@@ -172,7 +172,7 @@ const DocumentThumbnailImage = ({
} else { } else {
delete node.dataset.thumbnailAspect; delete node.dataset.thumbnailAspect;
} }
}, [aspectRatio]); }, [aspectRatio, visibilityRef]);
return ( return (
<div className="document-thumbnail-wrapper" ref={visibilityRef}> <div className="document-thumbnail-wrapper" ref={visibilityRef}>
+408 -132
View File
@@ -291,6 +291,90 @@ const resolveFolderRowKey = (folderId) =>
const hasFiles = (event) => const hasFiles = (event) =>
Array.from(event.dataTransfer?.types || []).includes('Files'); 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 = () => ({ const createRootNode = () => ({
id: 'root', id: 'root',
name: DEFAULT_FOLDER_NAME, name: DEFAULT_FOLDER_NAME,
@@ -591,89 +675,6 @@ const AppLayout = () => {
return getAssetFromVersion(doc.current_version || null, type); 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 bootstrapInitializedRef = useRef(false);
const selectionInitializedRef = useRef(false); const selectionInitializedRef = useRef(false);
const dragCounterRef = useRef(0); const dragCounterRef = useRef(0);
@@ -1557,7 +1558,7 @@ const AppLayout = () => {
return enriched; return enriched;
}, },
[api, assetManager, folderContents], [assetManager, folderContents],
); );
const isInvalidFolderDrop = useCallback( 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 () => { const refreshTags = useCallback(async () => {
@@ -1698,7 +1707,7 @@ const AppLayout = () => {
} }
notifyApiError(error, 'Unable to load tags.'); notifyApiError(error, 'Unable to load tags.');
} }
}, [api, notifyApiError]); }, [notifyApiError]);
const refreshCorrespondents = useCallback(async () => { const refreshCorrespondents = useCallback(async () => {
const requestTenantId = tenantIdRef.current; const requestTenantId = tenantIdRef.current;
@@ -1714,7 +1723,7 @@ const AppLayout = () => {
} }
notifyApiError(error, 'Unable to load correspondents.'); notifyApiError(error, 'Unable to load correspondents.');
} }
}, [api, notifyApiError]); }, [notifyApiError]);
const refreshWebdavTokens = useCallback(async () => { const refreshWebdavTokens = useCallback(async () => {
if (!token) { if (!token) {
@@ -1729,7 +1738,7 @@ const AppLayout = () => {
} finally { } finally {
setWebdavTokensLoading(false); setWebdavTokensLoading(false);
} }
}, [api, notifyApiError, token]); }, [notifyApiError, token]);
const createWebdavToken = useCallback( const createWebdavToken = useCallback(
async ({ label, expires_at } = {}) => { async ({ label, expires_at } = {}) => {
@@ -1766,7 +1775,7 @@ const AppLayout = () => {
setCreatingWebdavToken(false); setCreatingWebdavToken(false);
} }
}, },
[api, creatingWebdavToken, notifyApiError, refreshWebdavTokens, setStatusMessage], [creatingWebdavToken, notifyApiError, refreshWebdavTokens, setStatusMessage],
); );
const deleteWebdavToken = useCallback( const deleteWebdavToken = useCallback(
@@ -1787,7 +1796,7 @@ const AppLayout = () => {
setDeletingWebdavTokenId(null); setDeletingWebdavTokenId(null);
} }
}, },
[api, refreshWebdavTokens, notifyApiError, setStatusMessage], [refreshWebdavTokens, notifyApiError, setStatusMessage],
); );
const dismissCreatedWebdavToken = useCallback(() => { const dismissCreatedWebdavToken = useCallback(() => {
@@ -1836,7 +1845,7 @@ const AppLayout = () => {
throw new Error(message); throw new Error(message);
} }
}, },
[api, refreshTags, notifyApiError, setStatusMessage], [refreshTags, notifyApiError, setStatusMessage],
); );
const handleTagCreate = useCallback( const handleTagCreate = useCallback(
@@ -1852,7 +1861,7 @@ const AppLayout = () => {
throw new Error(message); throw new Error(message);
} }
}, },
[api, refreshTags, notifyApiError, setStatusMessage, tagManager], [refreshTags, notifyApiError, setStatusMessage, tagManager],
); );
const handleCorrespondentUpdate = useCallback( const handleCorrespondentUpdate = useCallback(
@@ -1885,7 +1894,7 @@ const AppLayout = () => {
throw new Error(message); throw new Error(message);
} }
}, },
[api, refreshCorrespondents, notifyApiError, setStatusMessage], [refreshCorrespondents, notifyApiError, setStatusMessage],
); );
const handleCorrespondentCreate = useCallback( const handleCorrespondentCreate = useCallback(
@@ -1905,7 +1914,7 @@ const AppLayout = () => {
throw new Error(message); throw new Error(message);
} }
}, },
[api, refreshCorrespondents, notifyApiError, setStatusMessage], [refreshCorrespondents, notifyApiError, setStatusMessage],
); );
const handleCorrespondentDelete = useCallback( const handleCorrespondentDelete = useCallback(
@@ -1939,13 +1948,11 @@ const AppLayout = () => {
throw new Error(message); throw new Error(message);
} }
}, },
[api, refreshCorrespondents, notifyApiError, setStatusMessage, mapDocumentCaches], [refreshCorrespondents, notifyApiError, setStatusMessage, mapDocumentCaches],
); );
async function handleDocumentCorrespondentAttach( const handleDocumentCorrespondentAttach = useCallback(
{ documentId, correspondentId }, async ({ documentId, correspondentId }, { notify = true, refresh = true } = {}) => {
{ notify = true, refresh = true } = {},
) {
if (!documentId || !correspondentId) { if (!documentId || !correspondentId) {
throw new Error('Missing document or correspondent.'); throw new Error('Missing document or correspondent.');
} }
@@ -1966,12 +1973,12 @@ const AppLayout = () => {
notifyApiError(error, message); notifyApiError(error, message);
throw new Error(message); throw new Error(message);
} }
} },
[notifyApiError, refreshCurrentFolder, setStatusMessage],
);
async function handleCorrespondentRemove( const handleCorrespondentRemove = useCallback(
{ documentId, correspondentId }, async ({ documentId, correspondentId }, { notify = true, refresh = true } = {}) => {
{ notify = true, refresh = true } = {},
) {
if (!documentId || !correspondentId) { if (!documentId || !correspondentId) {
throw new Error('Missing document or correspondent.'); throw new Error('Missing document or correspondent.');
} }
@@ -1989,7 +1996,9 @@ const AppLayout = () => {
notifyApiError(error, message); notifyApiError(error, message);
throw new Error(message); throw new Error(message);
} }
} },
[notifyApiError, refreshCurrentFolder, setStatusMessage],
);
const handleCorrespondentAdd = useCallback( const handleCorrespondentAdd = useCallback(
async ({ document, name, input }) => { async ({ document, name, input }) => {
@@ -2070,7 +2079,6 @@ const AppLayout = () => {
} }
}, },
[ [
api,
refreshTags, refreshTags,
notifyApiError, notifyApiError,
setStatusMessage, setStatusMessage,
@@ -2367,7 +2375,6 @@ const AppLayout = () => {
[ [
correspondentLookupByName, correspondentLookupByName,
handleCorrespondentCreate, handleCorrespondentCreate,
api,
refreshCurrentFolder, refreshCurrentFolder,
resolveTargetDocumentIds, resolveTargetDocumentIds,
setStatusMessage, setStatusMessage,
@@ -2414,7 +2421,7 @@ const AppLayout = () => {
setStatusMessage('No correspondents changed.', 'info'); setStatusMessage('No correspondents changed.', 'info');
} }
}, },
[api, refreshCurrentFolder, resolveTargetDocumentIds, setStatusMessage], [refreshCurrentFolder, resolveTargetDocumentIds, setStatusMessage],
); );
useEffect(() => { useEffect(() => {
@@ -2528,7 +2535,6 @@ const AppLayout = () => {
[ [
resolveTargetDocumentIds, resolveTargetDocumentIds,
tags, tags,
api,
refreshTags, refreshTags,
refreshCurrentFolder, refreshCurrentFolder,
notifyApiError, notifyApiError,
@@ -2629,7 +2635,7 @@ const AppLayout = () => {
setLoading(false); setLoading(false);
} }
}, },
[resolveTargetDocumentIds, api, notifyApiError, setStatusMessage], [resolveTargetDocumentIds, notifyApiError, setStatusMessage],
); );
const uploadFile = useCallback( const uploadFile = useCallback(
@@ -2727,12 +2733,7 @@ const AppLayout = () => {
throw error; throw error;
} }
}, },
[ [assetManager, setDocuments, setSearchResults, notifyApiError],
assetManager,
setDocuments,
setSearchResults,
notifyApiError,
],
); );
const dragPreviewRef = useRef(null); const dragPreviewRef = useRef(null);
@@ -3082,7 +3083,7 @@ const AppLayout = () => {
previewInflightRef.current.set(documentId, request); previewInflightRef.current.set(documentId, request);
return request; return request;
}, },
[previewEntries, notifyApiError, resolveApiPath], [previewEntries, notifyApiError],
); );
const extractFilesFromDataTransfer = useCallback(async (dataTransfer) => { const extractFilesFromDataTransfer = useCallback(async (dataTransfer) => {
@@ -3327,7 +3328,6 @@ const AppLayout = () => {
} }
}, },
[ [
api,
ensureFolderData, ensureFolderData,
refreshCurrentFolder, refreshCurrentFolder,
selectedFolder, selectedFolder,
@@ -3402,7 +3402,6 @@ const AppLayout = () => {
[ [
searchResults, searchResults,
documents, documents,
api,
assetManager, assetManager,
setDocuments, setDocuments,
selectedFolder, selectedFolder,
@@ -3665,7 +3664,7 @@ const AppLayout = () => {
return false; return false;
} }
}, },
[api, refreshTags, notifyApiError, setStatusMessage, applyTagRemovalToCaches], [refreshTags, notifyApiError, setStatusMessage, applyTagRemovalToCaches],
); );
const handleTagAdd = useCallback( const handleTagAdd = useCallback(
@@ -3734,7 +3733,6 @@ const AppLayout = () => {
} }
}, },
[ [
api,
refreshCurrentFolder, refreshCurrentFolder,
notifyApiError, notifyApiError,
setStatusMessage, setStatusMessage,
@@ -3899,7 +3897,7 @@ const AppLayout = () => {
setLoading(false); setLoading(false);
} }
}, },
[api, token, notifyApiError, setStatusMessage], [token, notifyApiError, setStatusMessage],
); );
const handleFolderCreate = useCallback( const handleFolderCreate = useCallback(
@@ -4379,7 +4377,8 @@ const AppLayout = () => {
} }
}, [appDispatch, setStatusMessage]); }, [appDispatch, setStatusMessage]);
const folderClickHandlers = { const folderClickHandlers = useMemo(
() => ({
onToggle: async (folderId) => { onToggle: async (folderId) => {
const node = folderNodes.get(folderId); const node = folderNodes.get(folderId);
const nextExpanded = !(node?.expanded ?? false); const nextExpanded = !(node?.expanded ?? false);
@@ -4533,6 +4532,177 @@ const AppLayout = () => {
onDragLeave: (event) => { onDragLeave: (event) => {
event.currentTarget.classList.remove('is-drop-target'); 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(() => { const selectedDocument = useMemo(() => {
@@ -4600,7 +4770,6 @@ const AppLayout = () => {
} }
}, },
[ [
api,
token, token,
documentLookup, documentLookup,
setStatusMessage, setStatusMessage,
@@ -5016,7 +5185,8 @@ const AppLayout = () => {
], ],
); );
const documentsTableProps = { const documentsTableProps = useMemo(
() => ({
currentFolderName, currentFolderName,
breadcrumbs, breadcrumbs,
onRefresh: refreshCurrentFolder, onRefresh: refreshCurrentFolder,
@@ -5066,9 +5236,56 @@ const AppLayout = () => {
viewMode: documentsViewMode, viewMode: documentsViewMode,
onViewModeChange: handleDocumentsViewModeChange, onViewModeChange: handleDocumentsViewModeChange,
onClearSelection: clearDocumentSelection, 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, folderNodes,
onToggle: folderClickHandlers.onToggle, onToggle: folderClickHandlers.onToggle,
onSelect: folderClickHandlers.onSelect, onSelect: folderClickHandlers.onSelect,
@@ -5102,7 +5319,39 @@ const AppLayout = () => {
activeTenantId: currentTenantId, activeTenantId: currentTenantId,
onSelectTenant: handleTenantSelect, onSelectTenant: handleTenantSelect,
onOpenSettings: openSettingsModal, 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(() => { const handleDetailPanelClose = useCallback(() => {
setDetailPanelOpen(false); setDetailPanelOpen(false);
@@ -5111,7 +5360,8 @@ const AppLayout = () => {
setDetailPanelDocs([]); setDetailPanelDocs([]);
}, [clearDocumentSelection]); }, [clearDocumentSelection]);
const detailPanelProps = { const detailPanelProps = useMemo(
() => ({
selectedDocuments: detailPanelSelectedDocuments, selectedDocuments: detailPanelSelectedDocuments,
tags, tags,
tagLookupById, tagLookupById,
@@ -5138,7 +5388,35 @@ const AppLayout = () => {
onFolderNavigate: selectFolder, onFolderNavigate: selectFolder,
onClose: handleDetailPanelClose, onClose: handleDetailPanelClose,
resolveFolderPath, 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( const skeuoWorkspaceProps = useMemo(
() => ({ () => ({
@@ -5210,7 +5488,6 @@ const AppLayout = () => {
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getDocumentAsset,
notifyApiError, notifyApiError,
resolveApiPath,
openTagsModal, openTagsModal,
openCorrespondentsModal, openCorrespondentsModal,
openSettingsModal, openSettingsModal,
@@ -5253,7 +5530,6 @@ const AppLayout = () => {
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getDocumentAsset,
notifyApiError, notifyApiError,
resolveApiPath,
openTagsModal, openTagsModal,
openCorrespondentsModal, openCorrespondentsModal,
openSettingsModal, openSettingsModal,
+3 -3
View File
@@ -1,4 +1,4 @@
import React, { useMemo } from 'react'; import React from 'react';
import { formatFileSize } from '../utils/format'; import { formatFileSize } from '../utils/format';
import { DownloadIcon, TextScanIcon, AnalyzeIcon, CloseIcon } from '../ui/icons'; import { DownloadIcon, TextScanIcon, AnalyzeIcon, CloseIcon } from '../ui/icons';
import { openOcrTextInNewTab } from '../utils/ocr'; import { openOcrTextInNewTab } from '../utils/ocr';
@@ -24,7 +24,7 @@ const PreviewWorkspace = ({
const tags = Array.isArray(document.tags) ? document.tags : []; const tags = Array.isArray(document.tags) ? document.tags : [];
const correspondents = Array.isArray(document.correspondents) ? document.correspondents : []; const correspondents = Array.isArray(document.correspondents) ? document.correspondents : [];
const metadataSummary = useMemo(() => { const metadataSummary = (() => {
const rows = []; const rows = [];
if (mime) rows.push(['Type', mime]); if (mime) rows.push(['Type', mime]);
if (sizeLabel) rows.push(['Size', sizeLabel]); if (sizeLabel) rows.push(['Size', sizeLabel]);
@@ -45,7 +45,7 @@ const PreviewWorkspace = ({
]); ]);
} }
return rows; return rows;
}, [mime, sizeLabel, issuedAt, createdAt, updatedAt, folderName, tags, correspondents]); })();
return ( return (
<section className="preview-workspace"> <section className="preview-workspace">