cleanup
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import useFileDrop from './useFileDrop';
|
||||
import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/appLayoutUtils';
|
||||
|
||||
const useDocumentUploads = ({
|
||||
apiClient,
|
||||
token,
|
||||
selectedFolder,
|
||||
currentFolderName,
|
||||
ensureFolderData,
|
||||
refreshCurrentFolder,
|
||||
notifyApiError,
|
||||
setStatusMessage,
|
||||
setLoading,
|
||||
shellRef,
|
||||
}) => {
|
||||
const [dropOverlayState, setDropOverlayState] = useState({
|
||||
active: false,
|
||||
folderName: DEFAULT_FOLDER_NAME,
|
||||
});
|
||||
const dragCounterRef = useRef(0);
|
||||
const folderPathCacheRef = useRef(new Map());
|
||||
|
||||
const uploadFile = useCallback(
|
||||
async (file, targetFolderId) => {
|
||||
if (!file || file.size === 0) {
|
||||
setStatusMessage('Skipped empty file.', 'error');
|
||||
return null;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, file.name);
|
||||
if (targetFolderId && targetFolderId !== 'root') {
|
||||
formData.append('folder_id', targetFolderId);
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, status } = await apiClient.post('/documents', formData);
|
||||
const duplicate = data?.reused || status === 200;
|
||||
setStatusMessage(
|
||||
duplicate
|
||||
? `${file.name} already exists; reused existing document.`
|
||||
: `Uploaded ${file.name}`,
|
||||
duplicate ? 'info' : 'success',
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.error || `Failed to upload ${file.name}.`;
|
||||
notifyApiError(error, message);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[apiClient, notifyApiError, setStatusMessage],
|
||||
);
|
||||
|
||||
const ensureFolderPathOnServer = useCallback(
|
||||
async (baseFolderId, segments) => {
|
||||
const trimmedSegments = segments.map((segment) => segment.trim()).filter(Boolean);
|
||||
if (trimmedSegments.length === 0) {
|
||||
return baseFolderId ?? null;
|
||||
}
|
||||
|
||||
const cacheKey = `${baseFolderId ?? 'ROOT'}:${trimmedSegments.join('/')}`;
|
||||
const cache = folderPathCacheRef.current;
|
||||
if (cache.has(cacheKey)) {
|
||||
return cache.get(cacheKey);
|
||||
}
|
||||
|
||||
const payload = {
|
||||
parent_id: baseFolderId && baseFolderId !== 'root' ? baseFolderId : null,
|
||||
segments: trimmedSegments,
|
||||
};
|
||||
|
||||
const { data } = await apiClient.post('/folders/path', payload);
|
||||
cache.set(cacheKey, data.folder.id);
|
||||
return data.folder.id;
|
||||
},
|
||||
[apiClient],
|
||||
);
|
||||
|
||||
const extractFilesFromDataTransfer = useCallback(async (dataTransfer) => {
|
||||
if (!dataTransfer) {
|
||||
throw new Error('No drop payload found.');
|
||||
}
|
||||
|
||||
const items = Array.from(dataTransfer.items || []);
|
||||
console.info('[Uploads] drop start', {
|
||||
items: items.length,
|
||||
files: (dataTransfer.files || []).length,
|
||||
});
|
||||
|
||||
const results = [];
|
||||
const seenKeys = new Set();
|
||||
|
||||
const pushFile = (file, ancestors = []) => {
|
||||
if (!file) return;
|
||||
const segments = (ancestors || []).filter(Boolean);
|
||||
const key = `${segments.join('/')}/${file.name}:${file.size}`;
|
||||
if (seenKeys.has(key)) {
|
||||
return;
|
||||
}
|
||||
seenKeys.add(key);
|
||||
results.push({ file, segments });
|
||||
};
|
||||
|
||||
const readAllEntries = async (reader) => {
|
||||
const entries = [];
|
||||
let batch = [];
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
batch = await new Promise((resolve, reject) => reader.readEntries(resolve, reject));
|
||||
if (batch.length) {
|
||||
entries.push(...batch);
|
||||
}
|
||||
} while (batch.length);
|
||||
return entries;
|
||||
};
|
||||
|
||||
const walkEntry = async (entry, ancestors = []) => {
|
||||
if (!entry) return;
|
||||
if (entry.isFile) {
|
||||
const file = await new Promise((resolve, reject) => {
|
||||
try {
|
||||
entry.file(resolve, reject);
|
||||
} catch (error) {
|
||||
console.warn('[Uploads] entry.file failed', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
pushFile(file, ancestors);
|
||||
return;
|
||||
}
|
||||
if (entry.isDirectory) {
|
||||
const nextAncestors = entry.name ? [...ancestors, entry.name] : [...ancestors];
|
||||
const reader = entry.createReader();
|
||||
const entries = await readAllEntries(reader);
|
||||
for (const child of entries) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await walkEntry(child, nextAncestors);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all(
|
||||
items.map(async (item, index) => {
|
||||
if (item.kind !== 'file') return;
|
||||
|
||||
const fileFromItem = typeof item.getAsFile === 'function' ? item.getAsFile() : null;
|
||||
if (fileFromItem) {
|
||||
const relativePath =
|
||||
typeof fileFromItem.webkitRelativePath === 'string'
|
||||
? fileFromItem.webkitRelativePath
|
||||
: '';
|
||||
const segments = relativePath
|
||||
? relativePath
|
||||
.split('/')
|
||||
.slice(0, -1)
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
pushFile(fileFromItem, segments);
|
||||
}
|
||||
|
||||
if (typeof item.webkitGetAsEntry === 'function') {
|
||||
try {
|
||||
const entry = item.webkitGetAsEntry();
|
||||
if (entry) {
|
||||
await walkEntry(entry, []);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[Uploads] webkitGetAsEntry failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileFromItem) {
|
||||
console.info('[Uploads] item missing file handle', index);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
Array.from(dataTransfer.files || []).forEach((file) => {
|
||||
if (!file) return;
|
||||
const relativePath =
|
||||
typeof file.webkitRelativePath === 'string' ? file.webkitRelativePath : '';
|
||||
const segments = relativePath
|
||||
? relativePath
|
||||
.split('/')
|
||||
.slice(0, -1)
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
pushFile(file, segments);
|
||||
});
|
||||
|
||||
if (!results.length) {
|
||||
throw new Error('No files detected in drop payload.');
|
||||
}
|
||||
|
||||
console.info('[Uploads] prepared files', results.length);
|
||||
|
||||
return results;
|
||||
}, []);
|
||||
|
||||
const handleFileDrop = useCallback(
|
||||
async (dataTransfer, targetFolderId) => {
|
||||
if (!token) {
|
||||
setStatusMessage('Please log in before uploading.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
folderPathCacheRef.current.clear();
|
||||
|
||||
let extracted;
|
||||
try {
|
||||
extracted = await extractFilesFromDataTransfer(dataTransfer);
|
||||
} catch (error) {
|
||||
const message = error.message || 'Failed to process dropped files.';
|
||||
notifyApiError(error, message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!extracted.length) {
|
||||
setStatusMessage('No files to upload.', 'info');
|
||||
return;
|
||||
}
|
||||
const baseFolderId =
|
||||
targetFolderId && targetFolderId !== 'root' ? targetFolderId : null;
|
||||
|
||||
for (const { file, segments } of extracted) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const destinationId = segments.length
|
||||
? await ensureFolderPathOnServer(baseFolderId, segments)
|
||||
: baseFolderId;
|
||||
|
||||
const uploadTarget =
|
||||
destinationId ??
|
||||
(targetFolderId && targetFolderId !== 'root' ? targetFolderId : 'root');
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await uploadFile(file, uploadTarget);
|
||||
}
|
||||
|
||||
await refreshCurrentFolder();
|
||||
|
||||
if (
|
||||
targetFolderId &&
|
||||
targetFolderId !== 'root' &&
|
||||
targetFolderId !== selectedFolder
|
||||
) {
|
||||
await ensureFolderData(targetFolderId, { force: true, prefetchDepth: 1 });
|
||||
}
|
||||
} catch (error) {
|
||||
notifyApiError(error, 'Failed to upload files.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
token,
|
||||
extractFilesFromDataTransfer,
|
||||
ensureFolderPathOnServer,
|
||||
uploadFile,
|
||||
refreshCurrentFolder,
|
||||
selectedFolder,
|
||||
ensureFolderData,
|
||||
notifyApiError,
|
||||
setStatusMessage,
|
||||
setLoading,
|
||||
],
|
||||
);
|
||||
|
||||
useFileDrop({
|
||||
shellRef,
|
||||
token,
|
||||
currentFolderName,
|
||||
selectedFolder,
|
||||
handleFileDrop,
|
||||
hasFiles,
|
||||
defaultFolderName: DEFAULT_FOLDER_NAME,
|
||||
dragCounterRef,
|
||||
dropOverlayState,
|
||||
setDropOverlayState,
|
||||
});
|
||||
|
||||
const resetUploadsState = useCallback(() => {
|
||||
dragCounterRef.current = 0;
|
||||
setDropOverlayState({ active: false, folderName: DEFAULT_FOLDER_NAME });
|
||||
}, []);
|
||||
|
||||
return {
|
||||
dropOverlayState,
|
||||
setDropOverlayState,
|
||||
dragCounterRef,
|
||||
handleFileDrop,
|
||||
uploadFile,
|
||||
extractFilesFromDataTransfer,
|
||||
resetUploadsState,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDocumentUploads;
|
||||
Reference in New Issue
Block a user