feat: update frontend document management components.
This commit is contained in:
@@ -202,32 +202,39 @@ class FoldersManager<T extends ManagedFolder = ManagedFolder> {
|
||||
return this.treePromise;
|
||||
}
|
||||
|
||||
this.treePromise = (async () => {
|
||||
try {
|
||||
const raw = await getFolderTree();
|
||||
const flattened = flattenFolderTree(raw);
|
||||
this.ingest(flattened);
|
||||
const rootsPromises = raw as FolderTreeNode[];
|
||||
const rootNode = createRootNode() as FolderTreeNode;
|
||||
|
||||
rootNode.children = rootsPromises;
|
||||
rootNode.hasChildren = rootsPromises.length > 0;
|
||||
rootNode.loaded = true;
|
||||
|
||||
this.treeSnapshot = [rootNode];
|
||||
this.emit();
|
||||
return [rootNode];
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch folder tree', error);
|
||||
return [];
|
||||
} finally {
|
||||
this.treePromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
this.treePromise = this.fetchTreeInternal();
|
||||
return this.treePromise;
|
||||
}
|
||||
|
||||
async refreshTree(): Promise<FolderTreeNode[]> {
|
||||
this.treePromise = this.fetchTreeInternal();
|
||||
return this.treePromise;
|
||||
}
|
||||
|
||||
private async fetchTreeInternal(): Promise<FolderTreeNode[]> {
|
||||
try {
|
||||
const raw = await getFolderTree();
|
||||
const flattened = flattenFolderTree(raw);
|
||||
this.ingest(flattened);
|
||||
const rootsPromises = raw as FolderTreeNode[];
|
||||
const rootNode = createRootNode() as FolderTreeNode;
|
||||
|
||||
rootNode.children = rootsPromises;
|
||||
rootNode.hasChildren = rootsPromises.length > 0;
|
||||
rootNode.loaded = true;
|
||||
|
||||
this.treeSnapshot = [rootNode];
|
||||
this.emit();
|
||||
return [rootNode];
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch folder tree', error);
|
||||
// On error, do not clear existing snapshot if this was a refresh
|
||||
return this.treeSnapshot.length > 0 ? this.treeSnapshot : [];
|
||||
} finally {
|
||||
this.treePromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
invalidateTree() {
|
||||
this.treeSnapshot = [];
|
||||
this.treePromise = null;
|
||||
|
||||
@@ -13,6 +13,8 @@ type CorrespondentAssignment = {
|
||||
correspondent_id?: Identifier;
|
||||
};
|
||||
|
||||
import type { DocumentsManagerInterface } from '../types/workspaceTypes';
|
||||
|
||||
interface UseBulkDocumentActionsArgs {
|
||||
resolveTargetDocumentIds: (ids?: Identifier[] | null) => Identifier[];
|
||||
correspondentLookupByName: Map<string, { id?: Identifier }>;
|
||||
@@ -22,7 +24,7 @@ interface UseBulkDocumentActionsArgs {
|
||||
handleDocumentsDelete: (ids: Identifier[], options?: MessageOptions) => Promise<boolean>;
|
||||
handleFolderDelete: (id: Identifier, options?: MessageOptions) => Promise<boolean>;
|
||||
clearDocumentSelection: () => void;
|
||||
updateDocumentCaches?: (id: Identifier, updater: (doc: any) => any) => void;
|
||||
documentsManager: DocumentsManagerInterface;
|
||||
}
|
||||
|
||||
const useBulkDocumentActions = ({
|
||||
@@ -34,7 +36,7 @@ const useBulkDocumentActions = ({
|
||||
handleDocumentsDelete,
|
||||
handleFolderDelete,
|
||||
clearDocumentSelection,
|
||||
updateDocumentCaches,
|
||||
documentsManager,
|
||||
}: UseBulkDocumentActionsArgs) => {
|
||||
const { showToast } = useStatusToast();
|
||||
|
||||
@@ -77,19 +79,22 @@ const useBulkDocumentActions = ({
|
||||
|
||||
const { assigned = 0, removed = 0 } = response;
|
||||
|
||||
if (updateDocumentCaches && target.id) {
|
||||
targets.forEach((docId) => {
|
||||
updateDocumentCaches(docId, (doc) => {
|
||||
if (!doc) return doc;
|
||||
const current = Array.isArray((doc as any).correspondents) ? (doc as any).correspondents : [];
|
||||
if (current.some((entry: any) => entry?.id === target.id)) {
|
||||
return doc;
|
||||
}
|
||||
return {
|
||||
...(doc as any),
|
||||
correspondents: [...current, { id: target.id, name: (target as any).name }],
|
||||
};
|
||||
});
|
||||
if (target.id) {
|
||||
const targetSet = new Set(targets);
|
||||
let targetId = target.id;
|
||||
let targetName = (target as any).name;
|
||||
|
||||
documentsManager.map((doc) => {
|
||||
if (!targetSet.has(doc.id as Identifier)) return undefined;
|
||||
|
||||
const current = Array.isArray((doc as any).correspondents) ? (doc as any).correspondents : [];
|
||||
if (current.some((entry: any) => entry?.id === targetId)) {
|
||||
return doc;
|
||||
}
|
||||
return {
|
||||
...(doc as any),
|
||||
correspondents: [...current, { id: targetId, name: targetName }],
|
||||
};
|
||||
});
|
||||
}
|
||||
const assignedSuffix = assigned === 1 ? '' : 's';
|
||||
@@ -115,7 +120,7 @@ const useBulkDocumentActions = ({
|
||||
handleCorrespondentCreate,
|
||||
resolveTargetDocumentIds,
|
||||
showToast,
|
||||
updateDocumentCaches,
|
||||
documentsManager,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -144,22 +149,22 @@ const useBulkDocumentActions = ({
|
||||
});
|
||||
|
||||
const { assigned = 0, removed = 0 } = response;
|
||||
if (updateDocumentCaches) {
|
||||
targets.forEach((docId) => {
|
||||
updateDocumentCaches(docId, (doc) => {
|
||||
if (!doc || !Array.isArray((doc as any).correspondents)) {
|
||||
return doc;
|
||||
}
|
||||
const filtered = (doc as any).correspondents.filter(
|
||||
(entry: any) =>
|
||||
entry && !normalizedAssignments.some((assignment) => assignment.correspondent_id === entry.id),
|
||||
);
|
||||
return filtered.length === (doc as any).correspondents.length
|
||||
? doc
|
||||
: { ...(doc as any), correspondents: filtered };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const targetSet = new Set(targets);
|
||||
documentsManager.map((doc) => {
|
||||
if (!targetSet.has(doc.id as Identifier)) return undefined;
|
||||
|
||||
if (!doc || !Array.isArray((doc as any).correspondents)) {
|
||||
return doc;
|
||||
}
|
||||
const filtered = (doc as any).correspondents.filter(
|
||||
(entry: any) =>
|
||||
entry && !normalizedAssignments.some((assignment) => assignment.correspondent_id === entry.id),
|
||||
);
|
||||
return filtered.length === (doc as any).correspondents.length
|
||||
? doc
|
||||
: { ...(doc as any), correspondents: filtered };
|
||||
});
|
||||
|
||||
if (removed > 0) {
|
||||
const removedSuffix = removed === 1 ? '' : 's';
|
||||
@@ -174,7 +179,7 @@ const useBulkDocumentActions = ({
|
||||
showToast('No correspondents changed.', 'info');
|
||||
}
|
||||
},
|
||||
[resolveTargetDocumentIds, showToast, updateDocumentCaches],
|
||||
[resolveTargetDocumentIds, showToast, documentsManager],
|
||||
);
|
||||
|
||||
const handleDeleteSelection = useCallback(async () => {
|
||||
|
||||
@@ -8,12 +8,12 @@ import useNotifyApiError from '../../hooks/useNotifyApiError';
|
||||
|
||||
interface UseCorrespondentsOptions {
|
||||
tenantIdRef: MutableRefObject<string | null>;
|
||||
mapDocumentCaches?: (mapper: (doc: any) => any) => void;
|
||||
documentsManager?: { map: (mapper: (doc: any) => any) => void };
|
||||
}
|
||||
|
||||
const useCorrespondents = ({
|
||||
tenantIdRef,
|
||||
mapDocumentCaches,
|
||||
documentsManager,
|
||||
}: UseCorrespondentsOptions) => {
|
||||
const [correspondents, setCorrespondents] = useState<Correspondent[]>([]);
|
||||
const { showToast } = useStatusToast();
|
||||
@@ -109,7 +109,7 @@ const useCorrespondents = ({
|
||||
await deleteCorrespondent(correspondentId);
|
||||
await refreshCorrespondents();
|
||||
|
||||
mapDocumentCaches?.(stripFromDoc);
|
||||
documentsManager?.map(stripFromDoc);
|
||||
|
||||
showToast('Correspondent deleted.', 'success');
|
||||
return true;
|
||||
@@ -119,7 +119,7 @@ const useCorrespondents = ({
|
||||
throw new Error(message);
|
||||
}
|
||||
},
|
||||
[mapDocumentCaches, notifyApiError, refreshCorrespondents, showToast],
|
||||
[documentsManager, notifyApiError, refreshCorrespondents, showToast],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -110,9 +110,9 @@ export const useDocumentMoveMutations = ({
|
||||
showToast(`Moved ${count} document${suffix} to ${targetLabel}.`, 'success');
|
||||
|
||||
if (updatedDocsMap.size) {
|
||||
documentsState.mapDocumentCaches((doc) => {
|
||||
documentsState.documentsManager.map((doc) => {
|
||||
if (!doc || !uniqueIdSet.has(doc.id as DocumentId)) {
|
||||
return doc;
|
||||
return undefined;
|
||||
}
|
||||
const updated = updatedDocsMap.get(doc.id as DocumentId);
|
||||
if (updated) {
|
||||
|
||||
@@ -89,7 +89,7 @@ const useDocumentMutations = ({
|
||||
handleDocumentTagDetach,
|
||||
} = useDocumentTagMutations({
|
||||
tagsState,
|
||||
documentsState: { updateDocumentCaches: documentsState.updateDocumentCaches },
|
||||
documentsState: { documentsManager: documentsState.documentsManager },
|
||||
});
|
||||
|
||||
const handleThumbnailRegeneration = useCallback(
|
||||
@@ -115,13 +115,10 @@ const useDocumentMutations = ({
|
||||
// Optimistic update could happen here but usually we wait for standardized confirmation
|
||||
// However workspace expects mutation here.
|
||||
try {
|
||||
// We use trashDocument for soft delete
|
||||
// If we want hard delete we need deleteDocument
|
||||
// Assuming trash for now as it makes sense for "Delete" action in UI unless specified
|
||||
await Promise.all(documentIds.map((id) => trashDocument(id)));
|
||||
|
||||
// Remove from local state
|
||||
documentsState.removeDocumentsFromCaches(documentIds);
|
||||
// Remove from local state and manager
|
||||
documentsState.documentsManager.remove(documentIds);
|
||||
|
||||
if (showMessage) {
|
||||
const count = documentIds.length;
|
||||
@@ -156,7 +153,8 @@ const useDocumentMutations = ({
|
||||
if (updatedDocument && documentsState.ingestDocuments) {
|
||||
documentsState.ingestDocuments([updatedDocument]);
|
||||
} else {
|
||||
documentsState.updateDocumentCaches(documentId, (doc) => {
|
||||
documentsState.documentsManager.map((doc) => {
|
||||
if (doc.id !== documentId) return undefined;
|
||||
if (updatedDocument) {
|
||||
return { ...doc, ...updatedDocument };
|
||||
}
|
||||
@@ -189,7 +187,8 @@ const useDocumentMutations = ({
|
||||
if (updatedDocument && documentsState.ingestDocuments) {
|
||||
documentsState.ingestDocuments([updatedDocument]);
|
||||
} else {
|
||||
documentsState.updateDocumentCaches(documentId, (doc) => {
|
||||
documentsState.documentsManager.map((doc) => {
|
||||
if (doc.id !== documentId) return undefined;
|
||||
if (updatedDocument) {
|
||||
return { ...doc, ...updatedDocument };
|
||||
}
|
||||
@@ -213,8 +212,6 @@ const useDocumentMutations = ({
|
||||
],
|
||||
);
|
||||
|
||||
// handleFolderDelete is removed from here
|
||||
|
||||
return {
|
||||
moveDocumentsToFolder,
|
||||
handleThumbnailRegeneration,
|
||||
|
||||
@@ -21,7 +21,7 @@ interface DocumentTagExtras {
|
||||
|
||||
interface UseDocumentTagMutationsArgs {
|
||||
tagsState: TagsState;
|
||||
documentsState: Pick<DocumentsState, 'updateDocumentCaches'>;
|
||||
documentsState: Pick<DocumentsState, 'documentsManager'>;
|
||||
}
|
||||
|
||||
export const useDocumentTagMutations = ({
|
||||
@@ -51,9 +51,9 @@ export const useDocumentTagMutations = ({
|
||||
|
||||
try {
|
||||
await addDocumentTags(documentId, [cachedTag.id]);
|
||||
documentsState.updateDocumentCaches(documentId, (doc) => {
|
||||
if (!doc) {
|
||||
return doc;
|
||||
documentsState.documentsManager.map((doc) => {
|
||||
if (doc.id !== documentId) {
|
||||
return undefined;
|
||||
}
|
||||
const currentTags = Array.isArray(doc.tags) ? doc.tags : [];
|
||||
if (currentTags.some((entry) => entry?.id === cachedTag.id)) {
|
||||
@@ -149,7 +149,10 @@ export const useDocumentTagMutations = ({
|
||||
try {
|
||||
await deleteDocumentTag(documentId, tagId);
|
||||
// Inlined applyTagRemovalToCaches logic
|
||||
documentsState.updateDocumentCaches(documentId, (doc) => {
|
||||
documentsState.documentsManager.map((doc) => {
|
||||
if (doc.id !== documentId) {
|
||||
return undefined;
|
||||
}
|
||||
if (!doc || !Array.isArray(doc.tags)) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
} from 'react';
|
||||
import DocumentsManager from '../DocumentsManager';
|
||||
import type { DocumentId } from '../../types/identifiers';
|
||||
@@ -18,91 +20,58 @@ const useDocuments = ({
|
||||
const managerRef = useRef(
|
||||
new DocumentsManager<Document>(fetchDocumentById),
|
||||
);
|
||||
const [documents, setDocumentsState] = useState<Document[]>([]);
|
||||
// Store only IDs in local state
|
||||
const [documentIds, setDocumentIds] = useState<DocumentId[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
managerRef.current.setFetcher(fetchDocumentById);
|
||||
}, [fetchDocumentById]);
|
||||
|
||||
// Subscribe to the manager for reactive updates
|
||||
const managerSnapshot = useSyncExternalStore(
|
||||
useCallback((cb) => managerRef.current.subscribe(cb), []),
|
||||
() => managerRef.current.getSnapshot(),
|
||||
() => managerRef.current.getSnapshot(),
|
||||
);
|
||||
|
||||
// Derive the full document objects from IDs + Snapshot
|
||||
const documents = useMemo(() => {
|
||||
if (!documentIds.length) return [];
|
||||
|
||||
// Efficiently map IDs to current document objects from the snapshot
|
||||
// If an ID is missing in the snapshot (unlikely if ingested correctly), return null/undefined and filter
|
||||
return documentIds
|
||||
.map(id => managerSnapshot.get(id))
|
||||
.filter((doc): doc is Document => Boolean(doc));
|
||||
}, [documentIds, managerSnapshot]);
|
||||
|
||||
// Keep a ref to the latest documents to avoid setDocuments dependency
|
||||
const documentsRef = useRef(documents);
|
||||
useEffect(() => {
|
||||
documentsRef.current = documents;
|
||||
}, [documents]);
|
||||
|
||||
const setDocuments = useCallback(
|
||||
(value: Document[] | ((prev: Document[]) => Document[])) => {
|
||||
setDocumentsState((prev) => {
|
||||
const resolved = typeof value === 'function' ? value(prev) : value;
|
||||
if (!Array.isArray(resolved)) {
|
||||
return resolved;
|
||||
}
|
||||
const { canonical } = managerRef.current.ingest(resolved);
|
||||
return canonical;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
// Support functional updates using the current derived documents as the previous state.
|
||||
// Use ref to avoid re-creating this callback when documents change.
|
||||
const prevDocs = documentsRef.current;
|
||||
const newDocs = typeof value === 'function' ? value(prevDocs) : value;
|
||||
|
||||
const mapDocumentCaches = useCallback(
|
||||
(mapper: (doc: Document) => Document | undefined) => {
|
||||
managerRef.current.map(mapper);
|
||||
const lookupSnapshot = managerRef.current.getSnapshot();
|
||||
|
||||
setDocumentsState((prev) => {
|
||||
if (!Array.isArray(prev) || prev.length === 0) {
|
||||
return prev;
|
||||
}
|
||||
let changed = false;
|
||||
const next = prev.map((doc) => {
|
||||
const id = doc?.id;
|
||||
if (id != null && lookupSnapshot.has(id as DocumentId)) {
|
||||
const canonical = lookupSnapshot.get(id as DocumentId) as Document;
|
||||
if (canonical !== doc) {
|
||||
changed = true;
|
||||
}
|
||||
return canonical;
|
||||
}
|
||||
const updated = mapper(doc);
|
||||
const nextDoc = updated === undefined ? doc : updated;
|
||||
if (nextDoc !== doc) {
|
||||
changed = true;
|
||||
}
|
||||
return nextDoc;
|
||||
});
|
||||
return changed ? next : prev;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const updateDocumentCaches = useCallback(
|
||||
(documentId, updater) => {
|
||||
if (!documentId) {
|
||||
if (!Array.isArray(newDocs)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapDocumentCaches((doc) => {
|
||||
if (!doc || doc.id !== documentId) {
|
||||
return doc;
|
||||
}
|
||||
const updated = updater(doc);
|
||||
return updated === undefined ? doc : updated;
|
||||
});
|
||||
const { canonical } = managerRef.current.ingest(newDocs);
|
||||
const newIds = canonical.map(d => d.id as DocumentId).filter(Boolean);
|
||||
setDocumentIds(newIds);
|
||||
},
|
||||
[mapDocumentCaches],
|
||||
);
|
||||
|
||||
const removeDocumentsFromLookup = useCallback(
|
||||
(documentIds: Array<DocumentId>) => {
|
||||
if (!Array.isArray(documentIds) || !documentIds.length) {
|
||||
return;
|
||||
}
|
||||
managerRef.current.remove(documentIds);
|
||||
},
|
||||
[],
|
||||
[] // Stable callback
|
||||
);
|
||||
|
||||
return {
|
||||
documents,
|
||||
setDocuments,
|
||||
removeDocumentsFromLookup,
|
||||
mapDocumentCaches,
|
||||
updateDocumentCaches,
|
||||
documentsManager: managerRef.current,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -223,9 +223,6 @@ const useDocumentsWorkspace = ({
|
||||
const {
|
||||
documents,
|
||||
setDocuments,
|
||||
removeDocumentsFromLookup,
|
||||
mapDocumentCaches,
|
||||
updateDocumentCaches,
|
||||
documentsManager,
|
||||
} = useDocuments({
|
||||
fetchDocumentById,
|
||||
@@ -264,6 +261,23 @@ const useDocumentsWorkspace = ({
|
||||
|
||||
const [currentSubfolders, setCurrentSubfolders] = useState<Array<{ id?: FolderNodeId; name?: string | null;[key: string]: unknown }>>([]);
|
||||
|
||||
const foldersSnapshot = useSyncExternalStore(
|
||||
useCallback((cb) => foldersManager.subscribe(cb), [foldersManager]),
|
||||
() => foldersManager.getSnapshot(),
|
||||
() => foldersManager.getSnapshot(),
|
||||
);
|
||||
|
||||
const visibleSubfolders = useMemo(() => {
|
||||
return currentSubfolders.map((folder) => {
|
||||
if (!folder?.id) return folder;
|
||||
const live = foldersSnapshot.get(folder.id);
|
||||
if (live) {
|
||||
return { ...folder, ...live };
|
||||
}
|
||||
return folder;
|
||||
});
|
||||
}, [currentSubfolders, foldersSnapshot]);
|
||||
|
||||
const reconcileSelectionWithFolderData = useCallback(
|
||||
(currentSelection: string[], docs: Document[], subfolders: any[]) => {
|
||||
const availableDocKeys = docs
|
||||
@@ -383,15 +397,30 @@ const useDocumentsWorkspace = ({
|
||||
const documentsFilter = documentsFilterValue;
|
||||
const showingSearchResults = searchResultIds !== null;
|
||||
|
||||
// Live Filter: Ensure we only show documents that actually belong to the current folder.
|
||||
// Since 'documents' is reactive, if a document is moved, its folder_id updates immediately.
|
||||
// We must filter out any documents that no longer match the selectedFolder.
|
||||
const liveFilteredDocuments = useMemo(() => {
|
||||
if (showingSearchResults) {
|
||||
return documents;
|
||||
}
|
||||
const targetFolder = selectedFolder || 'root';
|
||||
return documents.filter((doc) => {
|
||||
if (!doc) return false;
|
||||
const docFolder = doc.folder_id || 'root';
|
||||
return docFolder === targetFolder;
|
||||
});
|
||||
}, [documents, showingSearchResults, selectedFolder]);
|
||||
|
||||
const {
|
||||
viewDocuments,
|
||||
visibleEntryKeySet,
|
||||
} = useWorkspaceViewData({
|
||||
documents,
|
||||
documents: liveFilteredDocuments,
|
||||
documentLookup,
|
||||
searchResultIds,
|
||||
showingSearchResults,
|
||||
currentSubfolders,
|
||||
currentSubfolders: visibleSubfolders,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -445,7 +474,7 @@ const useDocumentsWorkspace = ({
|
||||
tenantIdRef,
|
||||
tagManager,
|
||||
setActiveTagFilters,
|
||||
mapDocumentCaches,
|
||||
documentsManager,
|
||||
});
|
||||
const {
|
||||
tags,
|
||||
@@ -476,7 +505,7 @@ const useDocumentsWorkspace = ({
|
||||
|
||||
const correspondentsStateRaw = useCorrespondents({
|
||||
tenantIdRef,
|
||||
mapDocumentCaches,
|
||||
documentsManager,
|
||||
});
|
||||
const {
|
||||
correspondents,
|
||||
@@ -495,7 +524,7 @@ const useDocumentsWorkspace = ({
|
||||
} = useDocumentCorrespondentActions({
|
||||
correspondents,
|
||||
handleCorrespondentCreate,
|
||||
updateDocumentCaches,
|
||||
documentsManager,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -548,7 +577,7 @@ const useDocumentsWorkspace = ({
|
||||
tagManager,
|
||||
refreshTags,
|
||||
resolveTargetDocumentIds,
|
||||
updateDocumentCaches,
|
||||
documentsManager,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -642,40 +671,11 @@ const useDocumentsWorkspace = ({
|
||||
}
|
||||
}, [appStatus, resetWorkspaceState, foldersManager]);
|
||||
|
||||
|
||||
const removeDocumentsFromCaches = useCallback(
|
||||
(documentIds: DocumentId[]) => {
|
||||
if (!documentIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idSet = new Set<DocumentId>(documentIds);
|
||||
|
||||
setDocuments((prev) => prev.filter((doc) => !idSet.has(doc.id)));
|
||||
setSearchResultIds((prev) => {
|
||||
if (!Array.isArray(prev)) {
|
||||
return prev;
|
||||
}
|
||||
const filtered = prev.filter((id) => !idSet.has(id as DocumentId));
|
||||
return filtered.length === prev.length ? prev : filtered;
|
||||
});
|
||||
|
||||
removeDocumentsFromLookup(Array.from(idSet));
|
||||
},
|
||||
[
|
||||
setDocuments,
|
||||
setSearchResultIds,
|
||||
removeDocumentsFromLookup,
|
||||
],
|
||||
);
|
||||
|
||||
const documentsState = {
|
||||
documentLookup,
|
||||
setDocuments,
|
||||
setSearchResultIds,
|
||||
removeDocumentsFromCaches,
|
||||
updateDocumentCaches,
|
||||
mapDocumentCaches,
|
||||
documentsManager,
|
||||
extractDocumentFromResponse,
|
||||
ingestDocuments: (docs: unknown[]) => documentsManager.ingest(docs),
|
||||
};
|
||||
@@ -732,7 +732,7 @@ const useDocumentsWorkspace = ({
|
||||
clearDocumentSelection,
|
||||
} = useDocumentsSelection({
|
||||
showingSearchResults,
|
||||
currentSubfolders,
|
||||
currentSubfolders: visibleSubfolders,
|
||||
visibleDocuments: viewDocuments,
|
||||
configureSelectionEnvironment,
|
||||
visibleEntryKeySet,
|
||||
@@ -834,7 +834,7 @@ const useDocumentsWorkspace = ({
|
||||
handleDocumentsDelete,
|
||||
handleFolderDelete,
|
||||
clearDocumentSelection,
|
||||
updateDocumentCaches,
|
||||
documentsManager,
|
||||
});
|
||||
|
||||
const ensureAssetUrl = useCallback(
|
||||
@@ -852,7 +852,10 @@ const useDocumentsWorkspace = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
updateDocumentCaches(documentId, (doc) => mergeAssetIntoDocument(doc, entry));
|
||||
documentsManager.map((doc) => {
|
||||
if (doc.id !== documentId) return undefined;
|
||||
return mergeAssetIntoDocument(doc, entry);
|
||||
});
|
||||
|
||||
return entry;
|
||||
} catch (error) {
|
||||
@@ -860,7 +863,7 @@ const useDocumentsWorkspace = ({
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[assetManager, updateDocumentCaches, notifyApiError],
|
||||
[assetManager, documentsManager, notifyApiError],
|
||||
);
|
||||
|
||||
const handlePromptCreateFolder = useCallback(async (parentId?: Identifier | null) => {
|
||||
@@ -1097,7 +1100,7 @@ const useDocumentsWorkspace = ({
|
||||
draggedFolderId,
|
||||
handlePromptCreateFolder,
|
||||
creatingFolder,
|
||||
currentSubfolders,
|
||||
currentSubfolders: visibleSubfolders,
|
||||
breadcrumbs,
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ interface UseTagsOptions {
|
||||
tagManager: TagManagerInterface;
|
||||
tenantIdRef: MutableRefObject<TenantId | null>;
|
||||
setActiveTagFilters: (updater: (prev: Array<TagId>) => Array<TagId>) => void;
|
||||
mapDocumentCaches?: (mapper: (doc: any) => any) => void;
|
||||
documentsManager?: { map: (mapper: (doc: any) => any) => void };
|
||||
}
|
||||
|
||||
const useTags = ({
|
||||
@@ -24,7 +24,7 @@ const useTags = ({
|
||||
tagManager,
|
||||
tenantIdRef,
|
||||
setActiveTagFilters,
|
||||
mapDocumentCaches,
|
||||
documentsManager,
|
||||
}: UseTagsOptions) => {
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const { showToast } = useStatusToast();
|
||||
@@ -115,7 +115,7 @@ const useTags = ({
|
||||
return { ...doc, tags: nextTags };
|
||||
};
|
||||
|
||||
mapDocumentCaches?.(stripTagFromDoc);
|
||||
documentsManager?.map(stripTagFromDoc);
|
||||
|
||||
await refreshTags();
|
||||
showToast('Tag deleted.', 'success');
|
||||
@@ -126,7 +126,7 @@ const useTags = ({
|
||||
throw new Error(message);
|
||||
}
|
||||
},
|
||||
[mapDocumentCaches, notifyApiError, refreshTags, setActiveTagFilters, showToast],
|
||||
[documentsManager, notifyApiError, refreshTags, setActiveTagFilters, showToast],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -12,20 +12,18 @@ interface CorrespondentOption {
|
||||
}
|
||||
|
||||
import useNotifyApiError from '../../../hooks/useNotifyApiError';
|
||||
import type { DocumentsManagerInterface } from '../../types/workspaceTypes';
|
||||
|
||||
interface UseDocumentCorrespondentActionsArgs {
|
||||
correspondents: CorrespondentOption[];
|
||||
handleCorrespondentCreate: (payload: { name: string }) => Promise<CorrespondentOption | null>;
|
||||
updateDocumentCaches?: (
|
||||
id: Identifier,
|
||||
updater: (doc: { correspondents?: CorrespondentOption[] } | null) => { correspondents?: CorrespondentOption[] } | null,
|
||||
) => void;
|
||||
documentsManager: DocumentsManagerInterface;
|
||||
}
|
||||
|
||||
const useDocumentCorrespondentActions = ({
|
||||
correspondents,
|
||||
handleCorrespondentCreate,
|
||||
updateDocumentCaches,
|
||||
documentsManager,
|
||||
}: UseDocumentCorrespondentActionsArgs) => {
|
||||
const { showToast } = useStatusToast();
|
||||
const notifyApiError = useNotifyApiError();
|
||||
@@ -54,24 +52,24 @@ const useDocumentCorrespondentActions = ({
|
||||
}
|
||||
try {
|
||||
await addDocumentCorrespondent(documentId, correspondentId);
|
||||
if (updateDocumentCaches) {
|
||||
const resolved = correspondent
|
||||
|| correspondents.find((entry) => entry?.id === correspondentId)
|
||||
|| null;
|
||||
updateDocumentCaches(documentId, (doc) => {
|
||||
if (!doc) {
|
||||
return doc;
|
||||
}
|
||||
const current = Array.isArray(doc.correspondents) ? doc.correspondents : [];
|
||||
if (current.some((entry) => entry?.id === correspondentId)) {
|
||||
return doc;
|
||||
}
|
||||
const nextEntry = resolved?.name
|
||||
? { id: resolved.id ?? correspondentId, name: resolved.name }
|
||||
: { id: correspondentId };
|
||||
return { ...doc, correspondents: [...current, nextEntry] };
|
||||
});
|
||||
}
|
||||
|
||||
const resolved = correspondent
|
||||
|| correspondents.find((entry) => entry?.id === correspondentId)
|
||||
|| null;
|
||||
|
||||
documentsManager.map((doc) => {
|
||||
if (doc.id !== documentId) return undefined;
|
||||
|
||||
const current = Array.isArray(doc.correspondents) ? doc.correspondents : [];
|
||||
if (current.some((entry) => entry?.id === correspondentId)) {
|
||||
return doc;
|
||||
}
|
||||
const nextEntry = resolved?.name
|
||||
? { id: resolved.id ?? correspondentId, name: resolved.name }
|
||||
: { id: correspondentId };
|
||||
return { ...doc, correspondents: [...current, nextEntry] };
|
||||
});
|
||||
|
||||
if (notify) {
|
||||
showToast('Correspondent assigned.', 'success');
|
||||
}
|
||||
@@ -82,7 +80,7 @@ const useDocumentCorrespondentActions = ({
|
||||
throw new Error(message);
|
||||
}
|
||||
},
|
||||
[correspondents, notifyApiError, showToast, updateDocumentCaches],
|
||||
[correspondents, notifyApiError, showToast, documentsManager],
|
||||
);
|
||||
|
||||
const handleCorrespondentRemove = useCallback(
|
||||
@@ -95,15 +93,16 @@ const useDocumentCorrespondentActions = ({
|
||||
}
|
||||
try {
|
||||
await removeDocumentCorrespondent(documentId, correspondentId);
|
||||
if (updateDocumentCaches) {
|
||||
updateDocumentCaches(documentId, (doc) => {
|
||||
if (!doc || !Array.isArray(doc.correspondents)) {
|
||||
return doc;
|
||||
}
|
||||
const filtered = doc.correspondents.filter((entry) => entry?.id !== correspondentId);
|
||||
return filtered.length === doc.correspondents.length ? doc : { ...doc, correspondents: filtered };
|
||||
});
|
||||
}
|
||||
|
||||
documentsManager.map((doc) => {
|
||||
if (doc.id !== documentId) return undefined;
|
||||
if (!doc || !Array.isArray(doc.correspondents)) {
|
||||
return doc;
|
||||
}
|
||||
const filtered = doc.correspondents.filter((entry) => entry?.id !== correspondentId);
|
||||
return filtered.length === doc.correspondents.length ? doc : { ...doc, correspondents: filtered };
|
||||
});
|
||||
|
||||
if (notify) {
|
||||
showToast('Correspondent removed.', 'success');
|
||||
}
|
||||
@@ -114,7 +113,7 @@ const useDocumentCorrespondentActions = ({
|
||||
throw new Error(message);
|
||||
}
|
||||
},
|
||||
[notifyApiError, showToast, updateDocumentCaches],
|
||||
[notifyApiError, showToast, documentsManager],
|
||||
);
|
||||
|
||||
const normalizeOption = (
|
||||
|
||||
@@ -103,7 +103,7 @@ const useFolderTreeActions = ({
|
||||
await moveFolderRequest(folderId, parent_id);
|
||||
|
||||
if (foldersManager) {
|
||||
foldersManager.invalidateTree();
|
||||
foldersManager.refreshTree();
|
||||
}
|
||||
|
||||
if (selectedFolder === folderId) {
|
||||
@@ -210,7 +210,7 @@ const useFolderTreeActions = ({
|
||||
// Ingest the new folder data immediately so it's available
|
||||
foldersManager.ingest([folderData]);
|
||||
// Force tree refresh to update structure
|
||||
foldersManager.invalidateTree();
|
||||
foldersManager.refreshTree();
|
||||
}
|
||||
|
||||
await selectFolder(folderData.id, { immediate: true });
|
||||
@@ -251,7 +251,7 @@ const useFolderTreeActions = ({
|
||||
|
||||
if (foldersManager) {
|
||||
foldersManager.remove([folderId]);
|
||||
foldersManager.invalidateTree();
|
||||
foldersManager.refreshTree();
|
||||
}
|
||||
|
||||
if (selectedFolder === folderId) {
|
||||
|
||||
@@ -16,13 +16,14 @@ interface TagManager {
|
||||
}
|
||||
|
||||
import useNotifyApiError from '../../../hooks/useNotifyApiError';
|
||||
import type { DocumentsManagerInterface } from '../../types/workspaceTypes';
|
||||
|
||||
interface UseDocumentTaggingArgs {
|
||||
tags: TagRecord[];
|
||||
tagManager: TagManager;
|
||||
refreshTags: () => Promise<void> | void;
|
||||
resolveTargetDocumentIds: (ids?: Identifier[] | null) => Identifier[];
|
||||
updateDocumentCaches?: (id: Identifier, updater: (doc: TagRecord | null) => TagRecord | null) => void;
|
||||
documentsManager: DocumentsManagerInterface;
|
||||
}
|
||||
|
||||
interface BulkTagOperationArgs {
|
||||
@@ -44,7 +45,7 @@ const useDocumentTagActions = ({
|
||||
tagManager,
|
||||
refreshTags,
|
||||
resolveTargetDocumentIds,
|
||||
updateDocumentCaches,
|
||||
documentsManager,
|
||||
}: UseDocumentTaggingArgs) => {
|
||||
const { showToast } = useStatusToast();
|
||||
const notifyApiError = useNotifyApiError();
|
||||
@@ -93,38 +94,39 @@ const useDocumentTagActions = ({
|
||||
}
|
||||
tagIds = Array.from(new Set(createdIds));
|
||||
|
||||
if (updateDocumentCaches) {
|
||||
const tagById = new Map<Identifier, TagRecord>();
|
||||
tags.forEach((tag) => {
|
||||
if (tag?.id != null) {
|
||||
tagById.set(tag.id, tag);
|
||||
}
|
||||
});
|
||||
createdTags.forEach((tag) => {
|
||||
if (tag?.id != null) {
|
||||
tagById.set(tag.id, tag);
|
||||
}
|
||||
});
|
||||
targetDocumentIds.forEach((docId) => {
|
||||
const tagById = new Map<Identifier, TagRecord>();
|
||||
tags.forEach((tag) => {
|
||||
if (tag?.id != null) {
|
||||
tagById.set(tag.id, tag);
|
||||
}
|
||||
});
|
||||
createdTags.forEach((tag) => {
|
||||
if (tag?.id != null) {
|
||||
tagById.set(tag.id, tag);
|
||||
}
|
||||
});
|
||||
|
||||
if (tagIds.length > 0) {
|
||||
const targetSet = new Set(targetDocumentIds);
|
||||
documentsManager.map((doc) => {
|
||||
if (!targetSet.has(doc.id as Identifier)) return undefined;
|
||||
|
||||
const currentTags = Array.isArray((doc as any).tags) ? (doc as any).tags : [];
|
||||
let nextTags = [...currentTags];
|
||||
let changed = false;
|
||||
|
||||
tagIds.forEach((tagId) => {
|
||||
const cachedTag = tagById.get(tagId);
|
||||
if (!cachedTag) {
|
||||
if (nextTags.some((entry: any) => entry?.id === tagId)) {
|
||||
return;
|
||||
}
|
||||
updateDocumentCaches(docId, (doc) => {
|
||||
if (!doc) {
|
||||
return doc;
|
||||
}
|
||||
const currentTags = Array.isArray((doc as any).tags) ? (doc as any).tags : [];
|
||||
if (currentTags.some((entry: any) => entry?.id === tagId)) {
|
||||
return doc;
|
||||
}
|
||||
return {
|
||||
...(doc as any),
|
||||
tags: [...currentTags, { ...cachedTag }],
|
||||
};
|
||||
});
|
||||
const cachedTag = tagById.get(tagId);
|
||||
if (cachedTag) {
|
||||
nextTags.push({ ...cachedTag });
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
return changed ? { ...(doc as any), tags: nextTags } : doc;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -141,21 +143,18 @@ const useDocumentTagActions = ({
|
||||
action,
|
||||
});
|
||||
|
||||
if (updateDocumentCaches) {
|
||||
targetDocumentIds.forEach((docId) => {
|
||||
tagIds.forEach((tagId) => {
|
||||
updateDocumentCaches(docId, (doc) => {
|
||||
if (!doc || !Array.isArray((doc as any).tags)) {
|
||||
return doc;
|
||||
}
|
||||
const currentTags = (doc as any).tags;
|
||||
if (action === 'remove') {
|
||||
const filtered = currentTags.filter((entry: any) => entry?.id !== tagId);
|
||||
return filtered.length === currentTags.length ? doc : { ...(doc as any), tags: filtered };
|
||||
}
|
||||
return doc;
|
||||
});
|
||||
});
|
||||
if (action === 'remove') {
|
||||
const targetSet = new Set(targetDocumentIds);
|
||||
const removeSet = new Set(tagIds);
|
||||
|
||||
documentsManager.map((doc) => {
|
||||
if (!targetSet.has(doc.id as Identifier)) return undefined;
|
||||
if (!doc || !Array.isArray((doc as any).tags)) {
|
||||
return doc;
|
||||
}
|
||||
const currentTags = (doc as any).tags;
|
||||
const filtered = currentTags.filter((entry: any) => !removeSet.has(entry?.id));
|
||||
return filtered.length === currentTags.length ? doc : { ...(doc as any), tags: filtered };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -178,7 +177,7 @@ const useDocumentTagActions = ({
|
||||
refreshTags,
|
||||
notifyApiError,
|
||||
tagManager,
|
||||
updateDocumentCaches,
|
||||
documentsManager,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -15,20 +15,12 @@ interface TagManager {
|
||||
buildPayload: (args: { label: string }) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
type DocumentCacheMapper = (
|
||||
doc: Document | null,
|
||||
) => Document | null;
|
||||
|
||||
type MapDocumentCaches = (mapper: DocumentCacheMapper) => void;
|
||||
|
||||
type UpdateDocumentCaches = (
|
||||
documentId: DocumentId,
|
||||
updater: DocumentCacheMapper,
|
||||
) => void;
|
||||
|
||||
|
||||
|
||||
type RemoveDocumentsFromCaches = (documentIds: DocumentId[]) => void;
|
||||
export interface DocumentsManagerInterface {
|
||||
map(mapper: (doc: Document) => Document | undefined): boolean;
|
||||
ingest(rawDocs: unknown[]): { canonical: Document[]; changed: boolean };
|
||||
remove(ids: Array<DocumentId>): boolean;
|
||||
}
|
||||
|
||||
type CloseDocumentPreview = () => void;
|
||||
|
||||
@@ -36,9 +28,7 @@ export interface DocumentsState {
|
||||
documentLookup: Map<DocumentId, Document>;
|
||||
setDocuments: Dispatch<SetStateAction<Document[]>>;
|
||||
setSearchResultIds: Dispatch<SetStateAction<DocumentId[] | null>>;
|
||||
removeDocumentsFromCaches: RemoveDocumentsFromCaches;
|
||||
updateDocumentCaches: UpdateDocumentCaches;
|
||||
mapDocumentCaches: MapDocumentCaches;
|
||||
documentsManager: DocumentsManagerInterface;
|
||||
extractDocumentFromResponse?: (payload: unknown) => Document | null;
|
||||
ingestDocuments?: (docs: unknown[]) => { canonical: Document[]; changed: boolean };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user