documentsmanager

This commit is contained in:
2025-11-19 23:36:04 +01:00
parent e1b47ed153
commit b76f1df27f
17 changed files with 564 additions and 300 deletions
+18 -99
View File
@@ -36,8 +36,13 @@ type NavigateHandler = (path: string, options?: { replace?: boolean }) => void;
interface UseDocumentPreviewArgs {
routeDocumentId?: DocumentId | null;
documents: DocumentLike[];
searchResults: DocumentLike[] | null;
documentsManager: {
getById: (id: DocumentId) => DocumentLike | null;
ensure: (id: DocumentId) => Promise<DocumentLike | null>;
getMany: (ids: DocumentId[]) => DocumentLike[];
subscribe: (listener: () => void) => () => void;
ingest: (docs: unknown[]) => { canonical: DocumentLike[]; changed: boolean };
};
selectedFolder?: FolderId | null;
assetManager: AssetManagerLike;
api: ApiClient;
@@ -55,7 +60,6 @@ interface UseDocumentPreviewArgs {
interface UseDocumentPreviewResult {
documentLinks: Map<DocumentId, DocumentLink>;
previewDocuments: Map<DocumentId, DocumentLike>;
ensureDownloadUrl: (documentId: DocumentId, options?: { force?: boolean }) => Promise<DocumentLink | null>;
ensurePreviewData: (documentId: DocumentId) => Promise<DocumentLike | null>;
openDocumentPreview: (documentId: DocumentId, options?: { replace?: boolean }) => void;
@@ -66,10 +70,9 @@ interface UseDocumentPreviewResult {
const useDocumentPreview = ({
routeDocumentId,
documents,
searchResults,
documentsManager,
selectedFolder,
assetManager,
assetManager: _assetManager,
api,
resolveApiPath,
notifyApiError,
@@ -80,7 +83,6 @@ const useDocumentPreview = ({
setActivePreviewId,
}: UseDocumentPreviewArgs): UseDocumentPreviewResult => {
const [documentLinks, setDocumentLinks] = useState<Map<DocumentId, DocumentLink>>(() => new Map());
const [previewDocuments, setPreviewDocuments] = useState<Map<DocumentId, DocumentLike>>(() => new Map());
const previewInflightRef = useRef<Map<DocumentId, Promise<DocumentLink | null>>>(new Map());
const previewReturnPathRef = useRef<string | null>(null);
@@ -110,35 +112,6 @@ const useDocumentPreview = ({
});
}, []);
const cachePreviewDocument = useCallback((doc: DocumentLike) => {
if (!doc?.id) {
return;
}
setPreviewDocuments((prev) => {
const existing = prev.get(doc.id);
if (existing === doc) {
return prev;
}
const next = new Map(prev);
next.set(doc.id, doc);
return next;
});
}, []);
const removeCachedPreviewDocument = useCallback((documentId: DocumentId) => {
if (!documentId) {
return;
}
setPreviewDocuments((prev) => {
if (!prev.has(documentId)) {
return prev;
}
const next = new Map(prev);
next.delete(documentId);
return next;
});
}, []);
const ensureDownloadUrl = useCallback(
async (documentId: DocumentId, { force = false }: { force?: boolean } = {}): Promise<DocumentLink | null> => {
if (!documentId) return null;
@@ -193,32 +166,22 @@ const useDocumentPreview = ({
async (documentId: DocumentId): Promise<DocumentLike | null> => {
if (!documentId) return null;
const findInCache = () => {
const pool = searchResults ?? documents;
return pool.find((item) => item.id === documentId) || null;
};
const findInCache = () => documentsManager.getById(documentId);
let doc = findInCache();
if (!doc) {
doc = await documentsManager.ensure(documentId);
}
if (!doc) {
const { data } = await api.get(`/documents/${documentId}`);
const hydratedDetail = assetManager.hydrateDetail(data);
const fetched = hydratedDetail?.document || (data as { document?: DocumentLike })?.document || data;
doc = fetched ? assetManager.hydrateDocument(fetched) || null : null;
const fetched = (data as { document?: DocumentLike })?.document || data;
const { canonical } = documentsManager.ingest([fetched as unknown]);
doc = (canonical[0] as DocumentLike | undefined) || null;
if (!doc) {
throw new Error('Document metadata unavailable.');
}
const existsInDocuments = documents.some((item) => item.id === doc?.id);
const existsInSearch = Array.isArray(searchResults)
? searchResults.some((item) => item.id === doc?.id)
: false;
if (doc?.id && (existsInDocuments || existsInSearch)) {
removeCachedPreviewDocument(doc.id);
} else {
cachePreviewDocument(doc);
}
}
if (!previewReturnPathRef.current) {
@@ -232,14 +195,10 @@ const useDocumentPreview = ({
return doc;
},
[
searchResults,
documents,
assetManager,
documentsManager,
ensureDownloadUrl,
setActivePreviewId,
api,
cachePreviewDocument,
removeCachedPreviewDocument,
],
);
@@ -270,21 +229,6 @@ const useDocumentPreview = ({
[navigate, selectedFolder],
);
useEffect(() => {
if (!routeDocumentId) {
return undefined;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeDocumentPreview();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [routeDocumentId, closeDocumentPreview]);
useEffect(() => {
if (!routeDocumentId) {
return undefined;
@@ -305,33 +249,8 @@ const useDocumentPreview = ({
};
}, [routeDocumentId, ensurePreviewData, notifyApiError, closeDocumentPreview]);
useEffect(() => {
setPreviewDocuments((prev) => {
if (!prev.size) {
return prev;
}
const next = new Map(prev);
let changed = false;
const prune = (list?: DocumentLike[] | null) => {
if (!Array.isArray(list)) {
return;
}
list.forEach((doc) => {
if (doc?.id && next.has(doc.id)) {
next.delete(doc.id);
changed = true;
}
});
};
prune(documents);
prune(searchResults);
return changed ? next : prev;
});
}, [documents, searchResults]);
return {
documentLinks,
previewDocuments,
ensureDownloadUrl,
ensurePreviewData,
openDocumentPreview,
+23 -20
View File
@@ -10,13 +10,8 @@ type ApiClient = {
get: <T = unknown>(url: string, config?: { params?: Record<string, unknown> }) => Promise<{ data: T }>;
};
type AssetManagerLike = {
hydrateDocuments: (payload: unknown[]) => DocumentLike[];
};
interface UseDocumentsSearchArgs {
api: ApiClient;
assetManager: AssetManagerLike;
token?: string | null;
selectedFolder?: Identifier | 'root' | null;
navigate?: (path: string, options?: { replace?: boolean }) => void;
@@ -28,13 +23,16 @@ interface UseDocumentsSearchArgs {
notifyApiError: (error: unknown, message: string) => void;
setLoading: (state: boolean) => void;
setSearchIncludeDescendants: (value: boolean) => void;
documentsManager: {
ingest: (docs: unknown[]) => { canonical: DocumentLike[]; changed: boolean };
};
}
interface UseDocumentsSearchResult {
searchQuery: string;
setSearchQuery: Dispatch<SetStateAction<string>>;
searchResults: DocumentLike[] | null;
setSearchResults: Dispatch<SetStateAction<DocumentLike[] | null>>;
searchResultIds: Identifier[] | null;
setSearchResultIds: Dispatch<SetStateAction<Identifier[] | null>>;
searchLoading: boolean;
setSearchLoading: Dispatch<SetStateAction<boolean>>;
activeTagFilters: Identifier[];
@@ -49,7 +47,7 @@ interface UseDocumentsSearchResult {
handleSearchSubmit: () => void;
documentsFilterValue: {
query: string;
searchResults: DocumentLike[] | null;
searchResultIds: Identifier[] | null;
searchLoading: boolean;
includeDescendants: boolean;
activeTagIds: Identifier[];
@@ -66,7 +64,6 @@ interface UseDocumentsSearchResult {
const useDocumentsSearch = ({
api,
assetManager,
token,
selectedFolder,
navigate,
@@ -78,11 +75,12 @@ const useDocumentsSearch = ({
notifyApiError,
setLoading,
setSearchIncludeDescendants,
documentsManager,
}: UseDocumentsSearchArgs): UseDocumentsSearchResult => {
const [searchQuery, setSearchQuery] = useState<string>('');
const [activeTagFilters, setActiveTagFilters] = useState<Identifier[]>([]);
const [activeCorrespondentFilters, setActiveCorrespondentFilters] = useState<Identifier[]>([]);
const [searchResults, setSearchResults] = useState<DocumentLike[] | null>(null);
const [searchResultIds, setSearchResultIds] = useState<Identifier[] | null>(null);
const [searchLoading, setSearchLoading] = useState<boolean>(false);
const toggleTagFilter = useCallback((tagId: Identifier) => {
@@ -122,6 +120,7 @@ const useDocumentsSearch = ({
setActiveCorrespondentFilters([]);
setSearchLoading(false);
setSearchIncludeDescendants(true);
setSearchResultIds(null);
}, [
setSearchIncludeDescendants,
]);
@@ -142,7 +141,7 @@ const useDocumentsSearch = ({
const documentsFilterValue = useMemo(
() => ({
query: searchQuery,
searchResults,
searchResultIds,
searchLoading,
includeDescendants: Boolean(searchIncludeDescendants),
activeTagIds: activeTagFilters,
@@ -157,7 +156,7 @@ const useDocumentsSearch = ({
}),
[
searchQuery,
searchResults,
searchResultIds,
searchLoading,
searchIncludeDescendants,
activeTagFilters,
@@ -176,7 +175,7 @@ const useDocumentsSearch = ({
if (!token) return undefined;
if (!isFilterActive) {
setSearchResults(null);
setSearchResultIds(null);
setSearchLoading(false);
return undefined;
}
@@ -225,17 +224,21 @@ const useDocumentsSearch = ({
const { data } = await api.get<unknown[]>('/documents', { params });
if (cancelled) return;
const results = assetManager.hydrateDocuments(data || []);
setSearchResults(results);
const results = Array.isArray(data) ? data : [];
const { canonical } = documentsManager.ingest(results);
const ids = canonical
.map((doc) => (doc?.id ?? null) as Identifier | null)
.filter((id): id is Identifier => id != null);
setSearchResultIds(ids);
if (!results.length) {
if (!ids.length) {
setSearchLoading(false);
return;
}
} catch (error) {
if (cancelled) return;
notifyApiError(error, 'Search failed. Please try again.');
setSearchResults(null);
setSearchResultIds(null);
} finally {
if (!cancelled && started) {
setLoading(false);
@@ -264,15 +267,15 @@ const useDocumentsSearch = ({
documentsSortDirection,
selectedFolder,
notifyApiError,
assetManager,
setLoading,
documentsManager,
]);
return {
searchQuery,
setSearchQuery,
searchResults,
setSearchResults,
searchResultIds,
setSearchResultIds,
searchLoading,
setSearchLoading,
activeTagFilters,
+2 -2
View File
@@ -123,7 +123,7 @@ export const useWorkspaceSurface = ({
const showPreviewWorkspace = Boolean(previewDocumentId);
const previewSurface = useMemo<WorkspaceSurface>(() => {
if (!showPreviewWorkspace || !previewWorkspaceDocument) {
if (!showPreviewWorkspace) {
return null;
}
@@ -145,7 +145,7 @@ export const useWorkspaceSurface = ({
return {
content: (
<DocumentViewerPanel
document={previewWorkspaceDocument}
document={previewWorkspaceDocument || null}
documentLink={documentLink}
hydrateDocument={ensurePreviewData}
tagLookupById={tagLookupById}