linting
This commit is contained in:
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -196,6 +196,7 @@ const useDocumentDrag = () => {
|
||||
recalcVisibleDocIds();
|
||||
},
|
||||
[
|
||||
bringToFront,
|
||||
canvasPadding,
|
||||
canvasSize.height,
|
||||
canvasSize.width,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -172,7 +172,7 @@ const DocumentThumbnailImage = ({
|
||||
} else {
|
||||
delete node.dataset.thumbnailAspect;
|
||||
}
|
||||
}, [aspectRatio]);
|
||||
}, [aspectRatio, visibilityRef]);
|
||||
|
||||
return (
|
||||
<div className="document-thumbnail-wrapper" ref={visibilityRef}>
|
||||
|
||||
+408
-132
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user