refactor(documents): unify document upload and deletion logic
Replace type-specific document add/remove functions with generalized uploadDocuments and deleteDocument methods. Consolidate file upload handling in DocumentUploader for improved maintainability. Introduce evictCachedDocument to streamline cache eviction across document types. Update DocumentContext to support batch uploads and single-method deletion, simplifying context API and reducing duplication.
This commit is contained in:
parent
cc868511d9
commit
d489356b00
4 changed files with 114 additions and 116 deletions
|
|
@ -228,13 +228,11 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
pdfDocs,
|
pdfDocs,
|
||||||
removePDFDocument,
|
|
||||||
isPDFLoading,
|
isPDFLoading,
|
||||||
epubDocs,
|
epubDocs,
|
||||||
removeEPUBDocument,
|
|
||||||
isEPUBLoading,
|
isEPUBLoading,
|
||||||
htmlDocs,
|
htmlDocs,
|
||||||
removeHTMLDocument,
|
deleteDocument,
|
||||||
isHTMLLoading,
|
isHTMLLoading,
|
||||||
} = useDocuments();
|
} = useDocuments();
|
||||||
|
|
||||||
|
|
@ -436,9 +434,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
const handleDelete = useCallback(async () => {
|
const handleDelete = useCallback(async () => {
|
||||||
if (!documentToDelete) return;
|
if (!documentToDelete) return;
|
||||||
try {
|
try {
|
||||||
if (documentToDelete.type === 'pdf') await removePDFDocument(documentToDelete.id);
|
await deleteDocument(documentToDelete.id);
|
||||||
else if (documentToDelete.type === 'epub') await removeEPUBDocument(documentToDelete.id);
|
|
||||||
else if (documentToDelete.type === 'html') await removeHTMLDocument(documentToDelete.id);
|
|
||||||
setFolders((prev) =>
|
setFolders((prev) =>
|
||||||
prev.map((f) => ({
|
prev.map((f) => ({
|
||||||
...f,
|
...f,
|
||||||
|
|
@ -451,7 +447,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to remove document:', err);
|
console.error('Failed to remove document:', err);
|
||||||
}
|
}
|
||||||
}, [documentToDelete, removePDFDocument, removeEPUBDocument, removeHTMLDocument]);
|
}, [deleteDocument, documentToDelete]);
|
||||||
|
|
||||||
const handleDeleteDoc = useCallback((doc: DocumentListDocument) => {
|
const handleDeleteDoc = useCallback((doc: DocumentListDocument) => {
|
||||||
setDocumentToDelete({ id: doc.id, name: doc.name, type: doc.type });
|
setDocumentToDelete({ id: doc.id, name: doc.name, type: doc.type });
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,7 @@ export function DocumentUploader({
|
||||||
const uploaderId = useId();
|
const uploaderId = useId();
|
||||||
const enableDocx = useFeatureFlag('enableDocxConversion');
|
const enableDocx = useFeatureFlag('enableDocxConversion');
|
||||||
const {
|
const {
|
||||||
addPDFDocument: addPDF,
|
uploadDocuments,
|
||||||
addEPUBDocument: addEPUB,
|
|
||||||
addHTMLDocument: addHTML,
|
|
||||||
refreshDocuments,
|
refreshDocuments,
|
||||||
} = useDocuments();
|
} = useDocuments();
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
@ -63,57 +61,60 @@ export function DocumentUploader({
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const docxFiles: File[] = [];
|
||||||
|
const regularFiles: File[] = [];
|
||||||
|
|
||||||
for (const file of acceptedFiles) {
|
for (const file of acceptedFiles) {
|
||||||
if (file.type === 'application/pdf') {
|
if (enableDocx && file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
|
||||||
emitBatchState({
|
docxFiles.push(file);
|
||||||
isActive: true,
|
|
||||||
totalFiles,
|
|
||||||
completedFiles,
|
|
||||||
phase: 'uploading',
|
|
||||||
currentFileName: file.name,
|
|
||||||
});
|
|
||||||
await addPDF(file);
|
|
||||||
completedFiles += 1;
|
|
||||||
} else if (file.type === 'application/epub+zip') {
|
|
||||||
emitBatchState({
|
|
||||||
isActive: true,
|
|
||||||
totalFiles,
|
|
||||||
completedFiles,
|
|
||||||
phase: 'uploading',
|
|
||||||
currentFileName: file.name,
|
|
||||||
});
|
|
||||||
await addEPUB(file);
|
|
||||||
completedFiles += 1;
|
|
||||||
} else if (file.type === 'text/plain' || file.type === 'text/markdown' || file.name.endsWith('.md')) {
|
|
||||||
emitBatchState({
|
|
||||||
isActive: true,
|
|
||||||
totalFiles,
|
|
||||||
completedFiles,
|
|
||||||
phase: 'uploading',
|
|
||||||
currentFileName: file.name,
|
|
||||||
});
|
|
||||||
await addHTML(file);
|
|
||||||
completedFiles += 1;
|
|
||||||
} else if (enableDocx && file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
|
|
||||||
// Preserve prior UX: show "Converting DOCX..." state rather than generic uploading.
|
|
||||||
setIsUploading(false);
|
|
||||||
setIsConverting(true);
|
|
||||||
emitBatchState({
|
|
||||||
isActive: true,
|
|
||||||
totalFiles,
|
|
||||||
completedFiles,
|
|
||||||
phase: 'converting',
|
|
||||||
currentFileName: file.name,
|
|
||||||
});
|
|
||||||
// Convert+upload directly on the server. Use sha(docx) as stable ID to avoid duplicates.
|
|
||||||
await uploadDocxAsPdf(file);
|
|
||||||
await refreshDocuments();
|
|
||||||
setIsConverting(false);
|
|
||||||
setIsUploading(true);
|
|
||||||
completedFiles += 1;
|
|
||||||
} else {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
file.type === 'application/pdf'
|
||||||
|
|| file.type === 'application/epub+zip'
|
||||||
|
|| file.type === 'text/plain'
|
||||||
|
|| file.type === 'text/markdown'
|
||||||
|
|| file.name.endsWith('.md')
|
||||||
|
) {
|
||||||
|
regularFiles.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regularFiles.length > 0) {
|
||||||
|
emitBatchState({
|
||||||
|
isActive: true,
|
||||||
|
totalFiles,
|
||||||
|
completedFiles,
|
||||||
|
phase: 'uploading',
|
||||||
|
currentFileName: regularFiles[0]?.name ?? null,
|
||||||
|
});
|
||||||
|
await uploadDocuments(regularFiles);
|
||||||
|
completedFiles += regularFiles.length;
|
||||||
|
emitBatchState({
|
||||||
|
isActive: true,
|
||||||
|
totalFiles,
|
||||||
|
completedFiles,
|
||||||
|
phase: 'uploading',
|
||||||
|
currentFileName: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of docxFiles) {
|
||||||
|
// Preserve prior UX: show "Converting DOCX..." state rather than generic uploading.
|
||||||
|
setIsUploading(false);
|
||||||
|
setIsConverting(true);
|
||||||
|
emitBatchState({
|
||||||
|
isActive: true,
|
||||||
|
totalFiles,
|
||||||
|
completedFiles,
|
||||||
|
phase: 'converting',
|
||||||
|
currentFileName: file.name,
|
||||||
|
});
|
||||||
|
await uploadDocxAsPdf(file);
|
||||||
|
await refreshDocuments();
|
||||||
|
setIsConverting(false);
|
||||||
|
setIsUploading(true);
|
||||||
|
completedFiles += 1;
|
||||||
|
|
||||||
emitBatchState({
|
emitBatchState({
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
|
@ -137,7 +138,7 @@ export function DocumentUploader({
|
||||||
currentFileName: null,
|
currentFileName: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [addHTML, addPDF, addEPUB, refreshDocuments, enableDocx, emitBatchState]);
|
}, [uploadDocuments, refreshDocuments, enableDocx, emitBatchState]);
|
||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
onDrop,
|
onDrop,
|
||||||
|
|
|
||||||
|
|
@ -3,32 +3,53 @@
|
||||||
import { createContext, useCallback, useContext, useEffect, useMemo, ReactNode } from 'react';
|
import { createContext, useCallback, useContext, useEffect, useMemo, ReactNode } from 'react';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import type { BaseDocument } from '@/types/documents';
|
import type { BaseDocument } from '@/types/documents';
|
||||||
import { listDocuments, uploadDocuments, deleteDocuments } from '@/lib/client/api/documents';
|
import {
|
||||||
import { putCachedEpub, putCachedHtml, putCachedPdf, evictCachedEpub, evictCachedHtml, evictCachedPdf } from '@/lib/client/cache/documents';
|
deleteDocuments as deleteServerDocuments,
|
||||||
|
listDocuments,
|
||||||
|
uploadDocuments as uploadServerDocuments,
|
||||||
|
} from '@/lib/client/api/documents';
|
||||||
|
import { cacheStoredDocumentFromBytes, evictCachedDocument } from '@/lib/client/cache/documents';
|
||||||
import { useAuthSession } from '@/hooks/useAuthSession';
|
import { useAuthSession } from '@/hooks/useAuthSession';
|
||||||
|
|
||||||
interface DocumentContextType {
|
interface DocumentContextType {
|
||||||
pdfDocs: Array<BaseDocument & { type: 'pdf' }>;
|
pdfDocs: Array<BaseDocument & { type: 'pdf' }>;
|
||||||
addPDFDocument: (file: File) => Promise<string>;
|
|
||||||
removePDFDocument: (id: string) => Promise<void>;
|
|
||||||
isPDFLoading: boolean;
|
isPDFLoading: boolean;
|
||||||
|
|
||||||
epubDocs: Array<BaseDocument & { type: 'epub' }>;
|
epubDocs: Array<BaseDocument & { type: 'epub' }>;
|
||||||
addEPUBDocument: (file: File) => Promise<string>;
|
|
||||||
removeEPUBDocument: (id: string) => Promise<void>;
|
|
||||||
isEPUBLoading: boolean;
|
isEPUBLoading: boolean;
|
||||||
|
|
||||||
htmlDocs: Array<BaseDocument & { type: 'html' }>;
|
htmlDocs: Array<BaseDocument & { type: 'html' }>;
|
||||||
addHTMLDocument: (file: File) => Promise<string>;
|
|
||||||
removeHTMLDocument: (id: string) => Promise<void>;
|
|
||||||
isHTMLLoading: boolean;
|
isHTMLLoading: boolean;
|
||||||
|
|
||||||
|
uploadDocuments: (files: File[]) => Promise<BaseDocument[]>;
|
||||||
|
deleteDocument: (id: string) => Promise<void>;
|
||||||
refreshDocuments: () => Promise<void>;
|
refreshDocuments: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DocumentContext = createContext<DocumentContextType | undefined>(undefined);
|
const DocumentContext = createContext<DocumentContextType | undefined>(undefined);
|
||||||
const DOCUMENTS_QUERY_KEY = 'documents';
|
const DOCUMENTS_QUERY_KEY = 'documents';
|
||||||
|
|
||||||
|
type SupportedDocument = BaseDocument & { type: 'pdf' | 'epub' | 'html' };
|
||||||
|
|
||||||
|
function mergeStoredDocuments(
|
||||||
|
previous: SupportedDocument[] | undefined,
|
||||||
|
uploaded: BaseDocument[],
|
||||||
|
): SupportedDocument[] {
|
||||||
|
const next = [...(previous ?? [])];
|
||||||
|
|
||||||
|
for (let index = uploaded.length - 1; index >= 0; index -= 1) {
|
||||||
|
const stored = uploaded[index];
|
||||||
|
if (stored.type !== 'pdf' && stored.type !== 'epub' && stored.type !== 'html') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const supportedStored = stored as SupportedDocument;
|
||||||
|
const withoutExisting = next.filter((document) => document.id !== supportedStored.id);
|
||||||
|
next.splice(0, next.length, supportedStored, ...withoutExisting);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
export function DocumentProvider({ children }: { children: ReactNode }) {
|
export function DocumentProvider({ children }: { children: ReactNode }) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: sessionData, isPending: isSessionPending } = useAuthSession();
|
const { data: sessionData, isPending: isSessionPending } = useAuthSession();
|
||||||
|
|
@ -81,67 +102,43 @@ export function DocumentProvider({ children }: { children: ReactNode }) {
|
||||||
return { pdfDocs, epubDocs, htmlDocs };
|
return { pdfDocs, epubDocs, htmlDocs };
|
||||||
}, [docs]);
|
}, [docs]);
|
||||||
|
|
||||||
const cacheUploaded = useCallback(async (stored: BaseDocument, file: File) => {
|
const uploadDocuments = useCallback(async (files: File[]): Promise<BaseDocument[]> => {
|
||||||
try {
|
if (files.length === 0) return [];
|
||||||
if (stored.type === 'pdf') {
|
|
||||||
await putCachedPdf(stored, await file.arrayBuffer());
|
|
||||||
} else if (stored.type === 'epub') {
|
|
||||||
await putCachedEpub(stored, await file.arrayBuffer());
|
|
||||||
} else if (stored.type === 'html') {
|
|
||||||
const buf = await file.arrayBuffer();
|
|
||||||
const decoded = new TextDecoder().decode(new Uint8Array(buf));
|
|
||||||
await putCachedHtml(stored, decoded);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Cache failures should not block uploads.
|
|
||||||
console.warn('Failed to cache uploaded document:', stored.id, err);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const addDocument = useCallback(async (file: File): Promise<string> => {
|
const stored = await uploadServerDocuments(files);
|
||||||
const [stored] = await uploadDocuments([file]);
|
await Promise.allSettled(
|
||||||
if (!stored) throw new Error('Upload succeeded but returned no document');
|
stored.map(async (document, index) => {
|
||||||
await cacheUploaded(stored, file);
|
const file = files[index];
|
||||||
const isSupported = stored.type === 'pdf' || stored.type === 'epub' || stored.type === 'html';
|
if (!file) return;
|
||||||
if (!isSupported) return stored.id;
|
await cacheStoredDocumentFromBytes(document, await file.arrayBuffer());
|
||||||
const supportedStored = stored as BaseDocument & { type: 'pdf' | 'epub' | 'html' };
|
}),
|
||||||
queryClient.setQueryData<Array<BaseDocument & { type: 'pdf' | 'epub' | 'html' }>>(documentsQueryKey, (prev = []) => {
|
|
||||||
const next = prev.filter((d) => d.id !== supportedStored.id);
|
|
||||||
return [supportedStored, ...next];
|
|
||||||
});
|
|
||||||
return stored.id;
|
|
||||||
}, [cacheUploaded, queryClient, documentsQueryKey]);
|
|
||||||
|
|
||||||
const addPDFDocument = useCallback(async (file: File) => addDocument(file), [addDocument]);
|
|
||||||
const addEPUBDocument = useCallback(async (file: File) => addDocument(file), [addDocument]);
|
|
||||||
const addHTMLDocument = useCallback(async (file: File) => addDocument(file), [addDocument]);
|
|
||||||
|
|
||||||
const removeById = useCallback(async (id: string) => {
|
|
||||||
await deleteDocuments({ ids: [id] });
|
|
||||||
await Promise.allSettled([evictCachedPdf(id), evictCachedEpub(id), evictCachedHtml(id)]);
|
|
||||||
queryClient.setQueryData<Array<BaseDocument & { type: 'pdf' | 'epub' | 'html' }>>(documentsQueryKey, (prev = []) =>
|
|
||||||
prev.filter((d) => d.id !== id),
|
|
||||||
);
|
);
|
||||||
}, [queryClient, documentsQueryKey]);
|
|
||||||
|
|
||||||
const removePDFDocument = useCallback(async (id: string) => removeById(id), [removeById]);
|
queryClient.setQueryData<SupportedDocument[]>(documentsQueryKey, (previous) =>
|
||||||
const removeEPUBDocument = useCallback(async (id: string) => removeById(id), [removeById]);
|
mergeStoredDocuments(previous, stored),
|
||||||
const removeHTMLDocument = useCallback(async (id: string) => removeById(id), [removeById]);
|
);
|
||||||
|
|
||||||
|
return stored;
|
||||||
|
}, [documentsQueryKey, queryClient]);
|
||||||
|
|
||||||
|
const deleteDocument = useCallback(async (id: string) => {
|
||||||
|
await deleteServerDocuments({ ids: [id] });
|
||||||
|
await evictCachedDocument(id);
|
||||||
|
queryClient.setQueryData<SupportedDocument[]>(documentsQueryKey, (previous = []) =>
|
||||||
|
previous.filter((document) => document.id !== id),
|
||||||
|
);
|
||||||
|
}, [documentsQueryKey, queryClient]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentContext.Provider value={{
|
<DocumentContext.Provider value={{
|
||||||
pdfDocs: docsByType.pdfDocs,
|
pdfDocs: docsByType.pdfDocs,
|
||||||
addPDFDocument,
|
|
||||||
removePDFDocument,
|
|
||||||
isPDFLoading: isLoading,
|
isPDFLoading: isLoading,
|
||||||
epubDocs: docsByType.epubDocs,
|
epubDocs: docsByType.epubDocs,
|
||||||
addEPUBDocument,
|
|
||||||
removeEPUBDocument,
|
|
||||||
isEPUBLoading: isLoading,
|
isEPUBLoading: isLoading,
|
||||||
htmlDocs: docsByType.htmlDocs,
|
htmlDocs: docsByType.htmlDocs,
|
||||||
addHTMLDocument,
|
|
||||||
removeHTMLDocument,
|
|
||||||
isHTMLLoading: isLoading,
|
isHTMLLoading: isLoading,
|
||||||
|
uploadDocuments,
|
||||||
|
deleteDocument,
|
||||||
refreshDocuments,
|
refreshDocuments,
|
||||||
|
|
||||||
}}>
|
}}>
|
||||||
|
|
|
||||||
4
src/lib/client/cache/documents.ts
vendored
4
src/lib/client/cache/documents.ts
vendored
|
|
@ -107,6 +107,10 @@ export async function evictCachedHtml(id: string): Promise<void> {
|
||||||
await removeHtmlDocument(id);
|
await removeHtmlDocument(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function evictCachedDocument(id: string): Promise<void> {
|
||||||
|
await Promise.allSettled([evictCachedPdf(id), evictCachedEpub(id), evictCachedHtml(id)]);
|
||||||
|
}
|
||||||
|
|
||||||
export async function clearDocumentCache(): Promise<void> {
|
export async function clearDocumentCache(): Promise<void> {
|
||||||
const { clearPdfDocuments, clearEpubDocuments, clearHtmlDocuments } = await import('@/lib/client/dexie');
|
const { clearPdfDocuments, clearEpubDocuments, clearHtmlDocuments } = await import('@/lib/client/dexie');
|
||||||
await Promise.all([clearPdfDocuments(), clearEpubDocuments(), clearHtmlDocuments()]);
|
await Promise.all([clearPdfDocuments(), clearEpubDocuments(), clearHtmlDocuments()]);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue