Remove asset/document hydration pipeline and introduce client-side cache updates for correspondents, tags, and search results
This commit is contained in:
@@ -5,37 +5,21 @@ type DocumentId = string | number;
|
||||
export type ManagedDocument = { id?: DocumentId | null } & Record<string, unknown>;
|
||||
|
||||
type FetchDocument = (id: DocumentId) => Promise<unknown>;
|
||||
type HydrateDocument<T extends ManagedDocument> = (payload: unknown) => T | null;
|
||||
type HydrateDocuments<T extends ManagedDocument> = (payload: unknown[]) => T[];
|
||||
|
||||
class DocumentsManager<T extends ManagedDocument = ManagedDocument> {
|
||||
private byId: Map<DocumentId, T>;
|
||||
|
||||
private fetcher?: FetchDocument;
|
||||
|
||||
private hydrateDocument?: HydrateDocument<T>;
|
||||
|
||||
private hydrateDocuments?: HydrateDocuments<T>;
|
||||
|
||||
private extractDocument?: HydrateDocument<T>;
|
||||
|
||||
private inflight: Map<DocumentId, Promise<T | null>>;
|
||||
|
||||
private listeners: Set<() => void>;
|
||||
|
||||
constructor(
|
||||
fetchDocument?: FetchDocument,
|
||||
options?: {
|
||||
hydrateDocument?: HydrateDocument<T>;
|
||||
hydrateDocuments?: HydrateDocuments<T>;
|
||||
extractDocument?: HydrateDocument<T>;
|
||||
},
|
||||
) {
|
||||
this.byId = new Map();
|
||||
this.fetcher = fetchDocument;
|
||||
this.hydrateDocument = options?.hydrateDocument;
|
||||
this.hydrateDocuments = options?.hydrateDocuments;
|
||||
this.extractDocument = options?.extractDocument;
|
||||
this.inflight = new Map();
|
||||
this.listeners = new Set();
|
||||
}
|
||||
@@ -54,19 +38,7 @@ class DocumentsManager<T extends ManagedDocument = ManagedDocument> {
|
||||
}
|
||||
|
||||
ingest(rawDocs: unknown[] = []): { canonical: T[]; changed: boolean } {
|
||||
const normalize = (docs: unknown[]) => {
|
||||
if (this.hydrateDocuments) {
|
||||
return this.hydrateDocuments(docs).filter(Boolean) as T[];
|
||||
}
|
||||
if (this.hydrateDocument) {
|
||||
return docs
|
||||
.map((doc) => this.hydrateDocument ? this.hydrateDocument(doc) : (doc as T | null))
|
||||
.filter(Boolean) as T[];
|
||||
}
|
||||
return docs.filter(Boolean) as T[];
|
||||
};
|
||||
|
||||
const docs = normalize(rawDocs);
|
||||
const docs = rawDocs.map((doc) => doc as T).filter(Boolean);
|
||||
let changed = false;
|
||||
let nextById = this.byId;
|
||||
const canonical: T[] = [];
|
||||
@@ -124,8 +96,7 @@ class DocumentsManager<T extends ManagedDocument = ManagedDocument> {
|
||||
const request = (async () => {
|
||||
try {
|
||||
const fetched = await fetcher(id);
|
||||
const extracted = this.extractDocument ? this.extractDocument(fetched) : fetched;
|
||||
const { canonical } = this.ingest([extracted as unknown]);
|
||||
const { canonical } = this.ingest([fetched as unknown]);
|
||||
return canonical[0] ?? null;
|
||||
} finally {
|
||||
this.inflight.delete(id);
|
||||
|
||||
@@ -21,7 +21,6 @@ interface UseBulkDocumentActionsArgs {
|
||||
resolveTargetDocumentIds: (ids?: Identifier[] | null) => Identifier[];
|
||||
correspondentLookupByName: Map<string, { id?: Identifier }>;
|
||||
handleCorrespondentCreate: (payload: { name: string }) => Promise<{ id?: Identifier } | null>;
|
||||
refreshCurrentFolder: () => Promise<void> | void;
|
||||
setStatusMessage: (message: string, variant?: string) => void;
|
||||
selectedDocumentIds?: Identifier[];
|
||||
selectedFolderIds?: Identifier[];
|
||||
@@ -29,6 +28,7 @@ interface UseBulkDocumentActionsArgs {
|
||||
handleFolderDelete: (id: Identifier, options?: { showMessage?: boolean; manageLoading?: boolean }) => Promise<boolean>;
|
||||
clearDocumentSelection: () => void;
|
||||
setLoading: (value: boolean) => void;
|
||||
updateDocumentCaches?: (id: Identifier, updater: (doc: any) => any) => void;
|
||||
}
|
||||
|
||||
const useBulkDocumentActions = ({
|
||||
@@ -36,7 +36,6 @@ const useBulkDocumentActions = ({
|
||||
resolveTargetDocumentIds,
|
||||
correspondentLookupByName,
|
||||
handleCorrespondentCreate,
|
||||
refreshCurrentFolder,
|
||||
setStatusMessage,
|
||||
selectedDocumentIds,
|
||||
selectedFolderIds,
|
||||
@@ -44,6 +43,7 @@ const useBulkDocumentActions = ({
|
||||
handleFolderDelete,
|
||||
clearDocumentSelection,
|
||||
setLoading,
|
||||
updateDocumentCaches,
|
||||
}: UseBulkDocumentActionsArgs) => {
|
||||
const handleBulkCorrespondentAdd = useCallback(
|
||||
async ({ name, input, documentIds }: { name?: string; input?: HTMLInputElement | null; documentIds?: Identifier[] }) => {
|
||||
@@ -70,21 +70,35 @@ const useBulkDocumentActions = ({
|
||||
if (!target?.id) {
|
||||
setStatusMessage('Unable to resolve correspondent.', 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await api.post<BulkAssignmentResponse>('/documents/bulk/correspondents', {
|
||||
document_ids: targets,
|
||||
assignments: [
|
||||
{
|
||||
correspondent_id: target.id,
|
||||
},
|
||||
],
|
||||
action: 'add',
|
||||
const response = await api.post<BulkAssignmentResponse>('/documents/bulk/correspondents', {
|
||||
document_ids: targets,
|
||||
assignments: [
|
||||
{
|
||||
correspondent_id: target.id,
|
||||
},
|
||||
],
|
||||
action: 'add',
|
||||
});
|
||||
|
||||
const { assigned = 0, removed = 0 } = 'data' in response ? response.data : 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 }],
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const { assigned = 0, removed = 0 } = 'data' in response ? response.data : response;
|
||||
|
||||
await refreshCurrentFolder();
|
||||
}
|
||||
const assignedSuffix = assigned === 1 ? '' : 's';
|
||||
if (removed > 0) {
|
||||
const removedSuffix = removed === 1 ? '' : 's';
|
||||
@@ -99,19 +113,19 @@ const useBulkDocumentActions = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (input) {
|
||||
input.value = '';
|
||||
}
|
||||
},
|
||||
[
|
||||
api,
|
||||
correspondentLookupByName,
|
||||
handleCorrespondentCreate,
|
||||
refreshCurrentFolder,
|
||||
resolveTargetDocumentIds,
|
||||
setStatusMessage,
|
||||
],
|
||||
);
|
||||
if (input) {
|
||||
input.value = '';
|
||||
}
|
||||
},
|
||||
[
|
||||
api,
|
||||
correspondentLookupByName,
|
||||
handleCorrespondentCreate,
|
||||
resolveTargetDocumentIds,
|
||||
setStatusMessage,
|
||||
updateDocumentCaches,
|
||||
],
|
||||
);
|
||||
|
||||
const handleBulkCorrespondentRemove = useCallback(
|
||||
async ({ assignments = [], documentIds }: { assignments?: CorrespondentAssignment[]; documentIds?: Identifier[] }) => {
|
||||
@@ -131,14 +145,29 @@ const useBulkDocumentActions = ({
|
||||
correspondent_id: entry.correspondent_id,
|
||||
}));
|
||||
|
||||
const response = await api.post<BulkAssignmentResponse>('/documents/bulk/correspondents', {
|
||||
document_ids: targets,
|
||||
assignments: normalizedAssignments,
|
||||
action: 'remove',
|
||||
});
|
||||
const response = await api.post<BulkAssignmentResponse>('/documents/bulk/correspondents', {
|
||||
document_ids: targets,
|
||||
assignments: normalizedAssignments,
|
||||
action: 'remove',
|
||||
});
|
||||
|
||||
const { assigned = 0, removed = 0 } = 'data' in response ? response.data : response;
|
||||
await refreshCurrentFolder();
|
||||
const { assigned = 0, removed = 0 } = 'data' in response ? response.data : 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 };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
const removedSuffix = removed === 1 ? '' : 's';
|
||||
@@ -153,7 +182,7 @@ const useBulkDocumentActions = ({
|
||||
setStatusMessage('No correspondents changed.', 'info');
|
||||
}
|
||||
},
|
||||
[api, refreshCurrentFolder, resolveTargetDocumentIds, setStatusMessage],
|
||||
[api, resolveTargetDocumentIds, setStatusMessage, updateDocumentCaches],
|
||||
);
|
||||
|
||||
const handleDeleteSelection = useCallback(async () => {
|
||||
|
||||
@@ -518,11 +518,33 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
||||
);
|
||||
|
||||
const gridIconSize = DEFAULT_GRID_ICON_SIZE;
|
||||
useEffect(() => {
|
||||
|
||||
const scrollToTop = useCallback(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = 0;
|
||||
}
|
||||
}, [viewMode]);
|
||||
}, []);
|
||||
|
||||
const searchKey = useMemo(
|
||||
() => (Array.isArray(searchResultIds) ? searchResultIds.join(':') : 'none'),
|
||||
[searchResultIds],
|
||||
);
|
||||
|
||||
const breadcrumbKey = useMemo(
|
||||
() => (Array.isArray(breadcrumbs) ? breadcrumbs.map((crumb) => crumb?.id ?? '').join(':') : 'none'),
|
||||
[breadcrumbs],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToTop();
|
||||
}, [
|
||||
scrollToTop,
|
||||
viewMode,
|
||||
showingSearchResults,
|
||||
searchKey,
|
||||
breadcrumbKey,
|
||||
]);
|
||||
|
||||
const isTagDragEvent = useCallback((event) => isTagTransferEvent(event), []);
|
||||
const ensureFocusedRowVisible = useCallback(() => {
|
||||
if (!focusedRowKey) return;
|
||||
|
||||
Reference in New Issue
Block a user