documentsmanager
This commit is contained in:
Generated
+39
@@ -14,6 +14,7 @@
|
|||||||
"pdfjs-dist": "^5.4.394",
|
"pdfjs-dist": "^5.4.394",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.9.5"
|
"react-router-dom": "^7.9.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -3094,6 +3095,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.18.1",
|
"version": "8.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
@@ -8732,6 +8739,29 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.9.5",
|
"version": "7.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz",
|
||||||
@@ -10489,6 +10519,15 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"pdfjs-dist": "^5.4.394",
|
"pdfjs-dist": "^5.4.394",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.9.5"
|
"react-router-dom": "^7.9.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -36,8 +36,13 @@ type NavigateHandler = (path: string, options?: { replace?: boolean }) => void;
|
|||||||
|
|
||||||
interface UseDocumentPreviewArgs {
|
interface UseDocumentPreviewArgs {
|
||||||
routeDocumentId?: DocumentId | null;
|
routeDocumentId?: DocumentId | null;
|
||||||
documents: DocumentLike[];
|
documentsManager: {
|
||||||
searchResults: DocumentLike[] | null;
|
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;
|
selectedFolder?: FolderId | null;
|
||||||
assetManager: AssetManagerLike;
|
assetManager: AssetManagerLike;
|
||||||
api: ApiClient;
|
api: ApiClient;
|
||||||
@@ -55,7 +60,6 @@ interface UseDocumentPreviewArgs {
|
|||||||
|
|
||||||
interface UseDocumentPreviewResult {
|
interface UseDocumentPreviewResult {
|
||||||
documentLinks: Map<DocumentId, DocumentLink>;
|
documentLinks: Map<DocumentId, DocumentLink>;
|
||||||
previewDocuments: Map<DocumentId, DocumentLike>;
|
|
||||||
ensureDownloadUrl: (documentId: DocumentId, options?: { force?: boolean }) => Promise<DocumentLink | null>;
|
ensureDownloadUrl: (documentId: DocumentId, options?: { force?: boolean }) => Promise<DocumentLink | null>;
|
||||||
ensurePreviewData: (documentId: DocumentId) => Promise<DocumentLike | null>;
|
ensurePreviewData: (documentId: DocumentId) => Promise<DocumentLike | null>;
|
||||||
openDocumentPreview: (documentId: DocumentId, options?: { replace?: boolean }) => void;
|
openDocumentPreview: (documentId: DocumentId, options?: { replace?: boolean }) => void;
|
||||||
@@ -66,10 +70,9 @@ interface UseDocumentPreviewResult {
|
|||||||
|
|
||||||
const useDocumentPreview = ({
|
const useDocumentPreview = ({
|
||||||
routeDocumentId,
|
routeDocumentId,
|
||||||
documents,
|
documentsManager,
|
||||||
searchResults,
|
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
assetManager,
|
assetManager: _assetManager,
|
||||||
api,
|
api,
|
||||||
resolveApiPath,
|
resolveApiPath,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
@@ -80,7 +83,6 @@ const useDocumentPreview = ({
|
|||||||
setActivePreviewId,
|
setActivePreviewId,
|
||||||
}: UseDocumentPreviewArgs): UseDocumentPreviewResult => {
|
}: UseDocumentPreviewArgs): UseDocumentPreviewResult => {
|
||||||
const [documentLinks, setDocumentLinks] = useState<Map<DocumentId, DocumentLink>>(() => new Map());
|
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 previewInflightRef = useRef<Map<DocumentId, Promise<DocumentLink | null>>>(new Map());
|
||||||
const previewReturnPathRef = useRef<string | null>(null);
|
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(
|
const ensureDownloadUrl = useCallback(
|
||||||
async (documentId: DocumentId, { force = false }: { force?: boolean } = {}): Promise<DocumentLink | null> => {
|
async (documentId: DocumentId, { force = false }: { force?: boolean } = {}): Promise<DocumentLink | null> => {
|
||||||
if (!documentId) return null;
|
if (!documentId) return null;
|
||||||
@@ -193,31 +166,21 @@ const useDocumentPreview = ({
|
|||||||
async (documentId: DocumentId): Promise<DocumentLike | null> => {
|
async (documentId: DocumentId): Promise<DocumentLike | null> => {
|
||||||
if (!documentId) return null;
|
if (!documentId) return null;
|
||||||
|
|
||||||
const findInCache = () => {
|
const findInCache = () => documentsManager.getById(documentId);
|
||||||
const pool = searchResults ?? documents;
|
|
||||||
return pool.find((item) => item.id === documentId) || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
let doc = findInCache();
|
let doc = findInCache();
|
||||||
|
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
const { data } = await api.get(`/documents/${documentId}`);
|
doc = await documentsManager.ensure(documentId);
|
||||||
const hydratedDetail = assetManager.hydrateDetail(data);
|
|
||||||
const fetched = hydratedDetail?.document || (data as { document?: DocumentLike })?.document || data;
|
|
||||||
doc = fetched ? assetManager.hydrateDocument(fetched) || null : null;
|
|
||||||
if (!doc) {
|
|
||||||
throw new Error('Document metadata unavailable.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const existsInDocuments = documents.some((item) => item.id === doc?.id);
|
if (!doc) {
|
||||||
const existsInSearch = Array.isArray(searchResults)
|
const { data } = await api.get(`/documents/${documentId}`);
|
||||||
? searchResults.some((item) => item.id === doc?.id)
|
const fetched = (data as { document?: DocumentLike })?.document || data;
|
||||||
: false;
|
const { canonical } = documentsManager.ingest([fetched as unknown]);
|
||||||
|
doc = (canonical[0] as DocumentLike | undefined) || null;
|
||||||
if (doc?.id && (existsInDocuments || existsInSearch)) {
|
if (!doc) {
|
||||||
removeCachedPreviewDocument(doc.id);
|
throw new Error('Document metadata unavailable.');
|
||||||
} else {
|
|
||||||
cachePreviewDocument(doc);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,14 +195,10 @@ const useDocumentPreview = ({
|
|||||||
return doc;
|
return doc;
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
searchResults,
|
documentsManager,
|
||||||
documents,
|
|
||||||
assetManager,
|
|
||||||
ensureDownloadUrl,
|
ensureDownloadUrl,
|
||||||
setActivePreviewId,
|
setActivePreviewId,
|
||||||
api,
|
api,
|
||||||
cachePreviewDocument,
|
|
||||||
removeCachedPreviewDocument,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -270,21 +229,6 @@ const useDocumentPreview = ({
|
|||||||
[navigate, selectedFolder],
|
[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(() => {
|
useEffect(() => {
|
||||||
if (!routeDocumentId) {
|
if (!routeDocumentId) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -305,33 +249,8 @@ const useDocumentPreview = ({
|
|||||||
};
|
};
|
||||||
}, [routeDocumentId, ensurePreviewData, notifyApiError, closeDocumentPreview]);
|
}, [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 {
|
return {
|
||||||
documentLinks,
|
documentLinks,
|
||||||
previewDocuments,
|
|
||||||
ensureDownloadUrl,
|
ensureDownloadUrl,
|
||||||
ensurePreviewData,
|
ensurePreviewData,
|
||||||
openDocumentPreview,
|
openDocumentPreview,
|
||||||
|
|||||||
@@ -10,13 +10,8 @@ type ApiClient = {
|
|||||||
get: <T = unknown>(url: string, config?: { params?: Record<string, unknown> }) => Promise<{ data: T }>;
|
get: <T = unknown>(url: string, config?: { params?: Record<string, unknown> }) => Promise<{ data: T }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AssetManagerLike = {
|
|
||||||
hydrateDocuments: (payload: unknown[]) => DocumentLike[];
|
|
||||||
};
|
|
||||||
|
|
||||||
interface UseDocumentsSearchArgs {
|
interface UseDocumentsSearchArgs {
|
||||||
api: ApiClient;
|
api: ApiClient;
|
||||||
assetManager: AssetManagerLike;
|
|
||||||
token?: string | null;
|
token?: string | null;
|
||||||
selectedFolder?: Identifier | 'root' | null;
|
selectedFolder?: Identifier | 'root' | null;
|
||||||
navigate?: (path: string, options?: { replace?: boolean }) => void;
|
navigate?: (path: string, options?: { replace?: boolean }) => void;
|
||||||
@@ -28,13 +23,16 @@ interface UseDocumentsSearchArgs {
|
|||||||
notifyApiError: (error: unknown, message: string) => void;
|
notifyApiError: (error: unknown, message: string) => void;
|
||||||
setLoading: (state: boolean) => void;
|
setLoading: (state: boolean) => void;
|
||||||
setSearchIncludeDescendants: (value: boolean) => void;
|
setSearchIncludeDescendants: (value: boolean) => void;
|
||||||
|
documentsManager: {
|
||||||
|
ingest: (docs: unknown[]) => { canonical: DocumentLike[]; changed: boolean };
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseDocumentsSearchResult {
|
interface UseDocumentsSearchResult {
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
setSearchQuery: Dispatch<SetStateAction<string>>;
|
setSearchQuery: Dispatch<SetStateAction<string>>;
|
||||||
searchResults: DocumentLike[] | null;
|
searchResultIds: Identifier[] | null;
|
||||||
setSearchResults: Dispatch<SetStateAction<DocumentLike[] | null>>;
|
setSearchResultIds: Dispatch<SetStateAction<Identifier[] | null>>;
|
||||||
searchLoading: boolean;
|
searchLoading: boolean;
|
||||||
setSearchLoading: Dispatch<SetStateAction<boolean>>;
|
setSearchLoading: Dispatch<SetStateAction<boolean>>;
|
||||||
activeTagFilters: Identifier[];
|
activeTagFilters: Identifier[];
|
||||||
@@ -49,7 +47,7 @@ interface UseDocumentsSearchResult {
|
|||||||
handleSearchSubmit: () => void;
|
handleSearchSubmit: () => void;
|
||||||
documentsFilterValue: {
|
documentsFilterValue: {
|
||||||
query: string;
|
query: string;
|
||||||
searchResults: DocumentLike[] | null;
|
searchResultIds: Identifier[] | null;
|
||||||
searchLoading: boolean;
|
searchLoading: boolean;
|
||||||
includeDescendants: boolean;
|
includeDescendants: boolean;
|
||||||
activeTagIds: Identifier[];
|
activeTagIds: Identifier[];
|
||||||
@@ -66,7 +64,6 @@ interface UseDocumentsSearchResult {
|
|||||||
|
|
||||||
const useDocumentsSearch = ({
|
const useDocumentsSearch = ({
|
||||||
api,
|
api,
|
||||||
assetManager,
|
|
||||||
token,
|
token,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
navigate,
|
navigate,
|
||||||
@@ -78,11 +75,12 @@ const useDocumentsSearch = ({
|
|||||||
notifyApiError,
|
notifyApiError,
|
||||||
setLoading,
|
setLoading,
|
||||||
setSearchIncludeDescendants,
|
setSearchIncludeDescendants,
|
||||||
|
documentsManager,
|
||||||
}: UseDocumentsSearchArgs): UseDocumentsSearchResult => {
|
}: UseDocumentsSearchArgs): UseDocumentsSearchResult => {
|
||||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||||
const [activeTagFilters, setActiveTagFilters] = useState<Identifier[]>([]);
|
const [activeTagFilters, setActiveTagFilters] = useState<Identifier[]>([]);
|
||||||
const [activeCorrespondentFilters, setActiveCorrespondentFilters] = 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 [searchLoading, setSearchLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
const toggleTagFilter = useCallback((tagId: Identifier) => {
|
const toggleTagFilter = useCallback((tagId: Identifier) => {
|
||||||
@@ -122,6 +120,7 @@ const useDocumentsSearch = ({
|
|||||||
setActiveCorrespondentFilters([]);
|
setActiveCorrespondentFilters([]);
|
||||||
setSearchLoading(false);
|
setSearchLoading(false);
|
||||||
setSearchIncludeDescendants(true);
|
setSearchIncludeDescendants(true);
|
||||||
|
setSearchResultIds(null);
|
||||||
}, [
|
}, [
|
||||||
setSearchIncludeDescendants,
|
setSearchIncludeDescendants,
|
||||||
]);
|
]);
|
||||||
@@ -142,7 +141,7 @@ const useDocumentsSearch = ({
|
|||||||
const documentsFilterValue = useMemo(
|
const documentsFilterValue = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
query: searchQuery,
|
query: searchQuery,
|
||||||
searchResults,
|
searchResultIds,
|
||||||
searchLoading,
|
searchLoading,
|
||||||
includeDescendants: Boolean(searchIncludeDescendants),
|
includeDescendants: Boolean(searchIncludeDescendants),
|
||||||
activeTagIds: activeTagFilters,
|
activeTagIds: activeTagFilters,
|
||||||
@@ -157,7 +156,7 @@ const useDocumentsSearch = ({
|
|||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
searchQuery,
|
searchQuery,
|
||||||
searchResults,
|
searchResultIds,
|
||||||
searchLoading,
|
searchLoading,
|
||||||
searchIncludeDescendants,
|
searchIncludeDescendants,
|
||||||
activeTagFilters,
|
activeTagFilters,
|
||||||
@@ -176,7 +175,7 @@ const useDocumentsSearch = ({
|
|||||||
if (!token) return undefined;
|
if (!token) return undefined;
|
||||||
|
|
||||||
if (!isFilterActive) {
|
if (!isFilterActive) {
|
||||||
setSearchResults(null);
|
setSearchResultIds(null);
|
||||||
setSearchLoading(false);
|
setSearchLoading(false);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -225,17 +224,21 @@ const useDocumentsSearch = ({
|
|||||||
const { data } = await api.get<unknown[]>('/documents', { params });
|
const { data } = await api.get<unknown[]>('/documents', { params });
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
const results = assetManager.hydrateDocuments(data || []);
|
const results = Array.isArray(data) ? data : [];
|
||||||
setSearchResults(results);
|
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);
|
setSearchLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
notifyApiError(error, 'Search failed. Please try again.');
|
notifyApiError(error, 'Search failed. Please try again.');
|
||||||
setSearchResults(null);
|
setSearchResultIds(null);
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled && started) {
|
if (!cancelled && started) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -264,15 +267,15 @@ const useDocumentsSearch = ({
|
|||||||
documentsSortDirection,
|
documentsSortDirection,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
assetManager,
|
|
||||||
setLoading,
|
setLoading,
|
||||||
|
documentsManager,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
searchQuery,
|
searchQuery,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
searchResults,
|
searchResultIds,
|
||||||
setSearchResults,
|
setSearchResultIds,
|
||||||
searchLoading,
|
searchLoading,
|
||||||
setSearchLoading,
|
setSearchLoading,
|
||||||
activeTagFilters,
|
activeTagFilters,
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export const useWorkspaceSurface = ({
|
|||||||
const showPreviewWorkspace = Boolean(previewDocumentId);
|
const showPreviewWorkspace = Boolean(previewDocumentId);
|
||||||
|
|
||||||
const previewSurface = useMemo<WorkspaceSurface>(() => {
|
const previewSurface = useMemo<WorkspaceSurface>(() => {
|
||||||
if (!showPreviewWorkspace || !previewWorkspaceDocument) {
|
if (!showPreviewWorkspace) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ export const useWorkspaceSurface = ({
|
|||||||
return {
|
return {
|
||||||
content: (
|
content: (
|
||||||
<DocumentViewerPanel
|
<DocumentViewerPanel
|
||||||
document={previewWorkspaceDocument}
|
document={previewWorkspaceDocument || null}
|
||||||
documentLink={documentLink}
|
documentLink={documentLink}
|
||||||
hydrateDocument={ensurePreviewData}
|
hydrateDocument={ensurePreviewData}
|
||||||
tagLookupById={tagLookupById}
|
tagLookupById={tagLookupById}
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ type WorkspaceSnapshotState = {
|
|||||||
|
|
||||||
interface DesktopWorkspaceProps {
|
interface DesktopWorkspaceProps {
|
||||||
documents?: DeskDocument[];
|
documents?: DeskDocument[];
|
||||||
searchResults?: DeskDocument[] | null;
|
|
||||||
onInspectDocument?: (...args: unknown[]) => void;
|
onInspectDocument?: (...args: unknown[]) => void;
|
||||||
onEntryPointer?: (...args: unknown[]) => void;
|
onEntryPointer?: (...args: unknown[]) => void;
|
||||||
onDocumentStackSelect?: (docIds: Identifier[]) => void;
|
onDocumentStackSelect?: (docIds: Identifier[]) => void;
|
||||||
@@ -195,7 +194,6 @@ const defaultGetDocumentAsset: GetAsset = () => null;
|
|||||||
|
|
||||||
const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
||||||
documents = [],
|
documents = [],
|
||||||
searchResults = null,
|
|
||||||
onInspectDocument = null,
|
onInspectDocument = null,
|
||||||
onEntryPointer = null,
|
onEntryPointer = null,
|
||||||
onDocumentStackSelect = null,
|
onDocumentStackSelect = null,
|
||||||
@@ -211,10 +209,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
documentLinks,
|
documentLinks,
|
||||||
ensureDownloadUrl,
|
ensureDownloadUrl,
|
||||||
}) => {
|
}) => {
|
||||||
const items = useMemo<DeskDocument[]>(
|
const items = useMemo<DeskDocument[]>(() => documents, [documents]);
|
||||||
() => (searchResults ? searchResults : documents),
|
|
||||||
[documents, searchResults],
|
|
||||||
);
|
|
||||||
|
|
||||||
const allowLayoutPersistence = Boolean(tenantId && viewId && viewId.startsWith('folder:'));
|
const allowLayoutPersistence = Boolean(tenantId && viewId && viewId.startsWith('folder:'));
|
||||||
const documentLinkMap = documentLinks instanceof Map ? documentLinks : null;
|
const documentLinkMap = documentLinks instanceof Map ? documentLinks : null;
|
||||||
@@ -264,9 +259,8 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
}, [engine, allowLayoutPersistence, tenantId, viewId]);
|
}, [engine, allowLayoutPersistence, tenantId, viewId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nextItems = searchResults ? searchResults : documents;
|
engine.setItems(items || []);
|
||||||
engine.setItems(nextItems || []);
|
}, [engine, items]);
|
||||||
}, [engine, documents, searchResults]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ type DocumentLink = {
|
|||||||
|
|
||||||
interface UseDetailWorkspaceArgs {
|
interface UseDetailWorkspaceArgs {
|
||||||
documents: DocumentLike[];
|
documents: DocumentLike[];
|
||||||
searchResults?: DocumentLike[] | null;
|
|
||||||
previewDocuments?: Map<Identifier, DocumentLike> | null;
|
|
||||||
selectionOrder: string[];
|
selectionOrder: string[];
|
||||||
selectedDocumentIds: Identifier[];
|
selectedDocumentIds: Identifier[];
|
||||||
documentLookup: Map<Identifier, DocumentLike>;
|
documentLookup: Map<Identifier, DocumentLike>;
|
||||||
@@ -77,8 +75,6 @@ interface UseDetailWorkspaceResult {
|
|||||||
|
|
||||||
const useDetailWorkspace = ({
|
const useDetailWorkspace = ({
|
||||||
documents,
|
documents,
|
||||||
searchResults,
|
|
||||||
previewDocuments,
|
|
||||||
selectionOrder,
|
selectionOrder,
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
documentLookup,
|
documentLookup,
|
||||||
@@ -245,17 +241,19 @@ const useDetailWorkspace = ({
|
|||||||
[folderNodes],
|
[folderNodes],
|
||||||
);
|
);
|
||||||
|
|
||||||
const documentLink = useMemo(() => (detailPanelDocument ? documentLinks.get(detailPanelDocument.id) || null : null), [detailPanelDocument, documentLinks]);
|
const documentLink = useMemo(
|
||||||
|
() => (detailPanelDocument ? documentLinks.get(detailPanelDocument.id) || null : null),
|
||||||
|
[detailPanelDocument, documentLinks],
|
||||||
|
);
|
||||||
|
|
||||||
const previewWorkspaceDocument = useMemo(() => {
|
const previewWorkspaceDocument = useMemo(() => {
|
||||||
if (!previewDocumentId) {
|
if (!previewDocumentId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const pool = searchResults ?? documents;
|
return documentLookup.get(previewDocumentId)
|
||||||
return pool.find((doc) => doc.id === previewDocumentId)
|
|| documents.find((doc) => doc.id === previewDocumentId)
|
||||||
|| previewDocuments?.get?.(previewDocumentId)
|
|
||||||
|| null;
|
|| null;
|
||||||
}, [previewDocumentId, searchResults, documents, previewDocuments]);
|
}, [previewDocumentId, documentLookup, documents]);
|
||||||
|
|
||||||
const previewActive = Boolean(previewDocumentId && previewWorkspaceDocument);
|
const previewActive = Boolean(previewDocumentId && previewWorkspaceDocument);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import { shallowEqual } from 'react-redux';
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private emit() {
|
||||||
|
this.listeners.forEach((fn) => fn());
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(listener: () => void) {
|
||||||
|
this.listeners.add(listener);
|
||||||
|
return () => this.listeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFetcher(fetchDocument?: FetchDocument) {
|
||||||
|
this.fetcher = fetchDocument;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
let changed = false;
|
||||||
|
let nextById = this.byId;
|
||||||
|
const canonical: T[] = [];
|
||||||
|
|
||||||
|
docs.forEach((doc) => {
|
||||||
|
const id = doc?.id;
|
||||||
|
if (id == null) {
|
||||||
|
canonical.push(doc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = nextById.get(id as DocumentId);
|
||||||
|
const merged = existing ? ({ ...existing, ...doc } as T) : ({ ...(doc as T) } as T);
|
||||||
|
const useExisting = existing && shallowEqual(existing, merged);
|
||||||
|
const nextDoc = useExisting ? (existing as T) : merged;
|
||||||
|
|
||||||
|
if (!useExisting) {
|
||||||
|
if (!changed) {
|
||||||
|
nextById = new Map(this.byId);
|
||||||
|
}
|
||||||
|
nextById.set(id as DocumentId, nextDoc);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
canonical.push(nextDoc);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this.byId = nextById;
|
||||||
|
this.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { canonical, changed };
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensure(id: DocumentId, fetcherOverride?: FetchDocument): Promise<T | null> {
|
||||||
|
if (id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = this.byId.get(id);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher = fetcherOverride || this.fetcher;
|
||||||
|
if (!fetcher) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inflight = this.inflight.get(id);
|
||||||
|
if (inflight) {
|
||||||
|
return inflight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = (async () => {
|
||||||
|
try {
|
||||||
|
const fetched = await fetcher(id);
|
||||||
|
const extracted = this.extractDocument ? this.extractDocument(fetched) : fetched;
|
||||||
|
const { canonical } = this.ingest([extracted as unknown]);
|
||||||
|
return canonical[0] ?? null;
|
||||||
|
} finally {
|
||||||
|
this.inflight.delete(id);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
this.inflight.set(id, request);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
map(mapper: (doc: T) => T | undefined): boolean {
|
||||||
|
if (!this.byId.size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
const next = new Map<DocumentId, T>();
|
||||||
|
this.byId.forEach((doc, key) => {
|
||||||
|
const updated = mapper(doc);
|
||||||
|
const nextDoc = updated === undefined ? doc : updated;
|
||||||
|
if (nextDoc !== doc) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
next.set(key, nextDoc ?? doc);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this.byId = next;
|
||||||
|
this.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(ids: Array<DocumentId>): boolean {
|
||||||
|
if (!Array.isArray(ids) || ids.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let changed = false;
|
||||||
|
let next = this.byId;
|
||||||
|
ids.forEach((id) => {
|
||||||
|
if (next.has(id)) {
|
||||||
|
if (!changed) {
|
||||||
|
next = new Map(this.byId);
|
||||||
|
}
|
||||||
|
next.delete(id);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (changed) {
|
||||||
|
this.byId = next;
|
||||||
|
this.emit();
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
getById(id: DocumentId): T | null {
|
||||||
|
return this.byId.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMany(ids: Array<DocumentId> = []): T[] {
|
||||||
|
return ids
|
||||||
|
.map((id) => this.byId.get(id) || null)
|
||||||
|
.filter((doc): doc is T => Boolean(doc));
|
||||||
|
}
|
||||||
|
|
||||||
|
getSnapshot(): Map<DocumentId, T> {
|
||||||
|
return this.byId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DocumentsManager;
|
||||||
@@ -4,7 +4,7 @@ type Identifier = string | number;
|
|||||||
|
|
||||||
export interface DocumentsFilterValue {
|
export interface DocumentsFilterValue {
|
||||||
query: string;
|
query: string;
|
||||||
searchResults: Array<Record<string, unknown>> | null;
|
searchResultIds: Array<string | number> | null;
|
||||||
searchLoading: boolean;
|
searchLoading: boolean;
|
||||||
includeDescendants: boolean;
|
includeDescendants: boolean;
|
||||||
activeTagIds: Identifier[];
|
activeTagIds: Identifier[];
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export interface UseDocumentsPanelPropsArgs {
|
|||||||
refreshCurrentFolder?: () => void | Promise<void>;
|
refreshCurrentFolder?: () => void | Promise<void>;
|
||||||
currentSubfolders?: unknown[];
|
currentSubfolders?: unknown[];
|
||||||
documents?: unknown[];
|
documents?: unknown[];
|
||||||
searchResults?: unknown[] | null;
|
searchResultIds?: Identifier[] | null;
|
||||||
folderClickHandlers: FolderClickHandlers;
|
folderClickHandlers: FolderClickHandlers;
|
||||||
selectFolder?: (...args: unknown[]) => void;
|
selectFolder?: (...args: unknown[]) => void;
|
||||||
handleFolderDragStart?: (...args: unknown[]) => void;
|
handleFolderDragStart?: (...args: unknown[]) => void;
|
||||||
@@ -80,7 +80,7 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => {
|
|||||||
refreshCurrentFolder,
|
refreshCurrentFolder,
|
||||||
currentSubfolders,
|
currentSubfolders,
|
||||||
documents,
|
documents,
|
||||||
searchResults,
|
searchResultIds,
|
||||||
folderClickHandlers,
|
folderClickHandlers,
|
||||||
selectFolder,
|
selectFolder,
|
||||||
handleFolderDragStart,
|
handleFolderDragStart,
|
||||||
@@ -130,7 +130,7 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => {
|
|||||||
onRefresh: refreshCurrentFolder,
|
onRefresh: refreshCurrentFolder,
|
||||||
subfolders: currentSubfolders,
|
subfolders: currentSubfolders,
|
||||||
documents,
|
documents,
|
||||||
searchResults,
|
searchResultIds,
|
||||||
onFolderSelect: selectFolder,
|
onFolderSelect: selectFolder,
|
||||||
onFolderDrop: folderClickHandlers.onDrop,
|
onFolderDrop: folderClickHandlers.onDrop,
|
||||||
onFolderDragOver: folderClickHandlers.onDragOver,
|
onFolderDragOver: folderClickHandlers.onDragOver,
|
||||||
@@ -211,7 +211,7 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => {
|
|||||||
openDocumentPreview,
|
openDocumentPreview,
|
||||||
refreshCurrentFolder,
|
refreshCurrentFolder,
|
||||||
searchLoading,
|
searchLoading,
|
||||||
searchResults,
|
searchResultIds,
|
||||||
selectFolder,
|
selectFolder,
|
||||||
tagLookupById,
|
tagLookupById,
|
||||||
tags,
|
tags,
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
breadcrumbs,
|
breadcrumbs,
|
||||||
subfolders,
|
subfolders,
|
||||||
documents,
|
documents,
|
||||||
searchResults,
|
searchResultIds,
|
||||||
onFolderSelect,
|
onFolderSelect,
|
||||||
onFolderDrop,
|
onFolderDrop,
|
||||||
onFolderDragOver,
|
onFolderDragOver,
|
||||||
@@ -104,10 +104,19 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
toggleTag: toggleTagFilter,
|
toggleTag: toggleTagFilter,
|
||||||
toggleCorrespondent: toggleCorrespondentFilter,
|
toggleCorrespondent: toggleCorrespondentFilter,
|
||||||
} = useDocumentsFilter();
|
} = useDocumentsFilter();
|
||||||
const showingSearchResults = searchResults !== null;
|
const searchDocuments = useMemo(
|
||||||
const rows = showingSearchResults ? searchResults : documents;
|
() =>
|
||||||
|
Array.isArray(searchResultIds)
|
||||||
|
? searchResultIds
|
||||||
|
.map((id) => documentLookup?.get?.(id) || null)
|
||||||
|
.filter((doc): doc is Record<string, unknown> => Boolean(doc))
|
||||||
|
: null,
|
||||||
|
[searchResultIds, documentLookup],
|
||||||
|
);
|
||||||
|
const showingSearchResults = Array.isArray(searchResultIds);
|
||||||
|
const rows = showingSearchResults && searchDocuments ? searchDocuments : documents;
|
||||||
const documentLinkMap = documentLinks instanceof Map ? documentLinks : null;
|
const documentLinkMap = documentLinks instanceof Map ? documentLinks : null;
|
||||||
const searchResultCount = Array.isArray(searchResults) ? searchResults.length : 0;
|
const searchResultCount = Array.isArray(searchResultIds) ? searchResultIds.length : 0;
|
||||||
const headerTitle = showingSearchResults
|
const headerTitle = showingSearchResults
|
||||||
? 'Search results'
|
? 'Search results'
|
||||||
: currentFolderName || 'Documents';
|
: currentFolderName || 'Documents';
|
||||||
@@ -201,7 +210,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
const selectionContextRef = useRef(null);
|
const selectionContextRef = useRef(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nextContext = showingSearchResults
|
const nextContext = showingSearchResults
|
||||||
? { type: 'search', marker: searchResults }
|
? { type: 'search', marker: searchResultIds }
|
||||||
: { type: 'folder', marker: currentFolderId || 'root' };
|
: { type: 'folder', marker: currentFolderId || 'root' };
|
||||||
const previous = selectionContextRef.current;
|
const previous = selectionContextRef.current;
|
||||||
selectionContextRef.current = nextContext;
|
selectionContextRef.current = nextContext;
|
||||||
@@ -213,7 +222,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
if (changed) {
|
if (changed) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
}
|
}
|
||||||
}, [showingSearchResults, currentFolderId, searchResults, clearSelection]);
|
}, [showingSearchResults, currentFolderId, searchResultIds, clearSelection]);
|
||||||
|
|
||||||
const entries = useMemo(() => {
|
const entries = useMemo(() => {
|
||||||
const list = [];
|
const list = [];
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ interface UseDocumentMutationsArgs {
|
|||||||
setSelectedFolder: Dispatch<SetStateAction<FolderId>>;
|
setSelectedFolder: Dispatch<SetStateAction<FolderId>>;
|
||||||
setDocuments: Dispatch<SetStateAction<DocumentLike[]>>;
|
setDocuments: Dispatch<SetStateAction<DocumentLike[]>>;
|
||||||
setFolderContents: Dispatch<SetStateAction<Map<FolderId, FolderContents>>>;
|
setFolderContents: Dispatch<SetStateAction<Map<FolderId, FolderContents>>>;
|
||||||
setSearchResults: Dispatch<SetStateAction<DocumentLike[] | null>>;
|
setSearchResultIds: Dispatch<SetStateAction<DocumentId[] | null>>;
|
||||||
setSelectedEntries: Dispatch<SetStateAction<string[]>>;
|
setSelectedEntries: Dispatch<SetStateAction<string[]>>;
|
||||||
setSelectionOrder: Dispatch<SetStateAction<string[]>>;
|
setSelectionOrder: Dispatch<SetStateAction<string[]>>;
|
||||||
selectionOrderRef: MutableRefObject<string[] | null>;
|
selectionOrderRef: MutableRefObject<string[] | null>;
|
||||||
@@ -190,7 +190,7 @@ const useDocumentMutations = ({
|
|||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
setDocuments,
|
setDocuments,
|
||||||
setFolderContents,
|
setFolderContents,
|
||||||
setSearchResults,
|
setSearchResultIds,
|
||||||
setSelectedEntries,
|
setSelectedEntries,
|
||||||
setSelectionOrder,
|
setSelectionOrder,
|
||||||
selectionOrderRef,
|
selectionOrderRef,
|
||||||
@@ -319,11 +319,11 @@ const useDocumentMutations = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (uniqueIdSet.size) {
|
if (uniqueIdSet.size) {
|
||||||
setSearchResults((prev) => {
|
setSearchResultIds((prev) => {
|
||||||
if (!Array.isArray(prev) || !prev.length) {
|
if (!Array.isArray(prev) || !prev.length) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
const filtered = prev.filter((doc) => doc && !uniqueIdSet.has(doc.id as DocumentId));
|
const filtered = prev.filter((id) => !uniqueIdSet.has(id as DocumentId));
|
||||||
return filtered.length === prev.length ? prev : filtered;
|
return filtered.length === prev.length ? prev : filtered;
|
||||||
});
|
});
|
||||||
setDocuments((prev) => prev.filter((doc) => !uniqueIdSet.has(doc.id as DocumentId)));
|
setDocuments((prev) => prev.filter((doc) => !uniqueIdSet.has(doc.id as DocumentId)));
|
||||||
@@ -387,7 +387,7 @@ const useDocumentMutations = ({
|
|||||||
folderLabelMap,
|
folderLabelMap,
|
||||||
ensureFolderData,
|
ensureFolderData,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
setSearchResults,
|
setSearchResultIds,
|
||||||
setDocuments,
|
setDocuments,
|
||||||
setFolderContents,
|
setFolderContents,
|
||||||
setSelectedEntries,
|
setSelectedEntries,
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
|
import {
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import DocumentsManager from '../../documents/DocumentsManager';
|
||||||
|
|
||||||
|
type DocumentId = string | number;
|
||||||
|
|
||||||
interface DocumentLike {
|
interface DocumentLike {
|
||||||
id?: string | number;
|
id?: DocumentId;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,36 +21,74 @@ interface FolderContentsEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface UseDocumentsOptions {
|
interface UseDocumentsOptions {
|
||||||
setSearchResults: Dispatch<SetStateAction<DocumentLike[] | null>>;
|
|
||||||
setFolderContents: Dispatch<SetStateAction<Map<string, FolderContentsEntry>>>;
|
setFolderContents: Dispatch<SetStateAction<Map<string, FolderContentsEntry>>>;
|
||||||
|
fetchDocumentById?: (id: DocumentId) => Promise<DocumentLike | null>;
|
||||||
|
hydrateDocument?: (payload: unknown) => DocumentLike | null;
|
||||||
|
hydrateDocuments?: (payload: unknown[]) => DocumentLike[];
|
||||||
|
extractDocument?: (payload: unknown) => DocumentLike | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useDocuments = ({ setSearchResults, setFolderContents }: UseDocumentsOptions) => {
|
const useDocuments = ({
|
||||||
const [documents, setDocuments] = useState<DocumentLike[]>([]);
|
setFolderContents,
|
||||||
|
fetchDocumentById,
|
||||||
|
hydrateDocument,
|
||||||
|
hydrateDocuments,
|
||||||
|
extractDocument,
|
||||||
|
}: UseDocumentsOptions) => {
|
||||||
|
const managerRef = useRef(
|
||||||
|
new DocumentsManager<DocumentLike>(fetchDocumentById, {
|
||||||
|
hydrateDocument,
|
||||||
|
hydrateDocuments,
|
||||||
|
extractDocument,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const [documents, setDocumentsState] = useState<DocumentLike[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
managerRef.current.setFetcher(fetchDocumentById);
|
||||||
|
}, [fetchDocumentById]);
|
||||||
|
|
||||||
|
const setDocuments = useCallback(
|
||||||
|
(value: DocumentLike[] | ((prev: DocumentLike[]) => DocumentLike[])) => {
|
||||||
|
setDocumentsState((prev) => {
|
||||||
|
const resolved = typeof value === 'function' ? value(prev) : value;
|
||||||
|
if (!Array.isArray(resolved)) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
const { canonical } = managerRef.current.ingest(resolved);
|
||||||
|
return canonical;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const mapDocumentCaches = useCallback(
|
const mapDocumentCaches = useCallback(
|
||||||
(mapper: (doc: DocumentLike) => DocumentLike | undefined) => {
|
(mapper: (doc: DocumentLike) => DocumentLike | undefined) => {
|
||||||
|
managerRef.current.map(mapper);
|
||||||
|
const lookupSnapshot = managerRef.current.getSnapshot();
|
||||||
|
|
||||||
const applyToList = (list?: DocumentLike[] | null) => {
|
setDocumentsState((prev) => {
|
||||||
let changed = false;
|
if (!Array.isArray(prev) || prev.length === 0) {
|
||||||
const safeList = Array.isArray(list) ? list : [];
|
|
||||||
const next = safeList.map((doc) => {
|
|
||||||
const updated = mapper(doc);
|
|
||||||
if (updated === undefined || updated === doc) {
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
changed = true;
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
return changed ? next : safeList;
|
|
||||||
};
|
|
||||||
|
|
||||||
setDocuments((prev) => applyToList(prev));
|
|
||||||
setSearchResults((prev) => {
|
|
||||||
if (!Array.isArray(prev)) {
|
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
return applyToList(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 DocumentLike;
|
||||||
|
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;
|
||||||
});
|
});
|
||||||
setFolderContents((prev) => {
|
setFolderContents((prev) => {
|
||||||
if (!prev.size) {
|
if (!prev.size) {
|
||||||
@@ -56,12 +104,20 @@ const useDocuments = ({ setSearchResults, setFolderContents }: UseDocumentsOptio
|
|||||||
}
|
}
|
||||||
let docsChanged = false;
|
let docsChanged = false;
|
||||||
const updatedDocs = docs.map((doc) => {
|
const updatedDocs = docs.map((doc) => {
|
||||||
const updated = mapper(doc);
|
const id = doc?.id;
|
||||||
if (updated === undefined || updated === doc) {
|
if (id != null && lookupSnapshot.has(id as DocumentId)) {
|
||||||
return doc;
|
const canonical = lookupSnapshot.get(id as DocumentId) as DocumentLike;
|
||||||
}
|
if (canonical !== doc) {
|
||||||
docsChanged = true;
|
docsChanged = true;
|
||||||
return updated;
|
}
|
||||||
|
return canonical;
|
||||||
|
}
|
||||||
|
const updated = mapper(doc);
|
||||||
|
const nextDoc = updated === undefined ? doc : updated;
|
||||||
|
if (nextDoc !== doc) {
|
||||||
|
docsChanged = true;
|
||||||
|
}
|
||||||
|
return nextDoc;
|
||||||
});
|
});
|
||||||
if (docsChanged) {
|
if (docsChanged) {
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -73,7 +129,7 @@ const useDocuments = ({ setSearchResults, setFolderContents }: UseDocumentsOptio
|
|||||||
return changed ? next : prev;
|
return changed ? next : prev;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setFolderContents, setSearchResults],
|
[setFolderContents],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateDocumentCaches = useCallback(
|
const updateDocumentCaches = useCallback(
|
||||||
@@ -93,11 +149,23 @@ const useDocuments = ({ setSearchResults, setFolderContents }: UseDocumentsOptio
|
|||||||
[mapDocumentCaches],
|
[mapDocumentCaches],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const removeDocumentsFromLookup = useCallback(
|
||||||
|
(documentIds: Array<DocumentId>) => {
|
||||||
|
if (!Array.isArray(documentIds) || !documentIds.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
managerRef.current.remove(documentIds);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
documents,
|
documents,
|
||||||
setDocuments,
|
setDocuments,
|
||||||
|
removeDocumentsFromLookup,
|
||||||
mapDocumentCaches,
|
mapDocumentCaches,
|
||||||
updateDocumentCaches,
|
updateDocumentCaches,
|
||||||
|
documentsManager: managerRef.current,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import {
|
||||||
|
MutableRefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useSyncExternalStore,
|
||||||
|
} from 'react';
|
||||||
import {
|
import {
|
||||||
matchPath,
|
matchPath,
|
||||||
useLocation,
|
useLocation,
|
||||||
@@ -220,10 +228,20 @@ const useDocumentsWorkspace = ({
|
|||||||
if (!payload) {
|
if (!payload) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const hydratedDetail = assetManager.hydrateDetail(payload);
|
return payload.document || payload;
|
||||||
return hydratedDetail?.document || payload.document || payload;
|
|
||||||
},
|
},
|
||||||
[assetManager],
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchDocumentById = useCallback(
|
||||||
|
async (documentId: DocumentId) => {
|
||||||
|
if (!documentId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { data } = await api.get(`/documents/${documentId}`);
|
||||||
|
return extractDocumentFromResponse(data);
|
||||||
|
},
|
||||||
|
[extractDocumentFromResponse],
|
||||||
);
|
);
|
||||||
|
|
||||||
const tagManagerRef = useRef(null);
|
const tagManagerRef = useRef(null);
|
||||||
@@ -290,21 +308,27 @@ const useDocumentsWorkspace = ({
|
|||||||
folderContentsRef.current = folderContents;
|
folderContentsRef.current = folderContents;
|
||||||
}, [folderContents]);
|
}, [folderContents]);
|
||||||
|
|
||||||
const setSearchResultsRef = useRef<(value: unknown) => void>(() => {});
|
|
||||||
const setSearchResultsProxy = useCallback((value) => {
|
|
||||||
setSearchResultsRef.current(value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
documents,
|
documents,
|
||||||
setDocuments,
|
setDocuments,
|
||||||
|
removeDocumentsFromLookup,
|
||||||
mapDocumentCaches,
|
mapDocumentCaches,
|
||||||
updateDocumentCaches,
|
updateDocumentCaches,
|
||||||
|
documentsManager,
|
||||||
} = useDocuments({
|
} = useDocuments({
|
||||||
setSearchResults: setSearchResultsProxy,
|
|
||||||
setFolderContents,
|
setFolderContents,
|
||||||
|
fetchDocumentById,
|
||||||
|
hydrateDocument: (payload) => assetManager.hydrateDocument(payload),
|
||||||
|
hydrateDocuments: (payload) => assetManager.hydrateDocuments(payload),
|
||||||
|
extractDocument: extractDocumentFromResponse,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const documentLookup = useSyncExternalStore(
|
||||||
|
(onStoreChange) => documentsManager.subscribe(onStoreChange),
|
||||||
|
() => documentsManager.getSnapshot(),
|
||||||
|
() => documentsManager.getSnapshot(),
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
folderNodes,
|
folderNodes,
|
||||||
setFolderNodes,
|
setFolderNodes,
|
||||||
@@ -338,8 +362,8 @@ const useDocumentsWorkspace = ({
|
|||||||
const {
|
const {
|
||||||
searchQuery,
|
searchQuery,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
searchResults,
|
searchResultIds,
|
||||||
setSearchResults,
|
setSearchResultIds,
|
||||||
searchLoading,
|
searchLoading,
|
||||||
activeTagFilters,
|
activeTagFilters,
|
||||||
setActiveTagFilters,
|
setActiveTagFilters,
|
||||||
@@ -349,7 +373,6 @@ const useDocumentsWorkspace = ({
|
|||||||
documentsFilterValue,
|
documentsFilterValue,
|
||||||
} = useDocumentsSearch({
|
} = useDocumentsSearch({
|
||||||
api,
|
api,
|
||||||
assetManager,
|
|
||||||
token,
|
token,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
navigate,
|
navigate,
|
||||||
@@ -361,17 +384,66 @@ const useDocumentsWorkspace = ({
|
|||||||
notifyApiError,
|
notifyApiError,
|
||||||
setLoading,
|
setLoading,
|
||||||
setSearchIncludeDescendants,
|
setSearchIncludeDescendants,
|
||||||
|
documentsManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentsFilter = documentsFilterValue;
|
const documentsFilter = documentsFilterValue;
|
||||||
|
|
||||||
|
const [visibleDocumentIds, setVisibleDocumentIds] = useState<DocumentId[]>([]);
|
||||||
|
|
||||||
|
const showingSearchResults = searchResultIds !== null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSearchResultsRef.current = setSearchResults;
|
const arraysEqual = (a: DocumentId[], b: DocumentId[]) =>
|
||||||
}, [setSearchResults]);
|
a.length === b.length && a.every((value, index) => value === b[index]);
|
||||||
|
|
||||||
|
if (showingSearchResults && Array.isArray(searchResultIds)) {
|
||||||
|
const ids = searchResultIds.filter((id): id is DocumentId => id != null);
|
||||||
|
setVisibleDocumentIds((prev) => (arraysEqual(prev, ids) ? prev : ids));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderIds = documents
|
||||||
|
.map((doc) => (doc?.id ?? null) as DocumentId | null)
|
||||||
|
.filter((id): id is DocumentId => id != null);
|
||||||
|
setVisibleDocumentIds((prev) => (arraysEqual(prev, folderIds) ? prev : folderIds));
|
||||||
|
}, [showingSearchResults, searchResultIds, documents]);
|
||||||
|
|
||||||
|
const viewDocuments = useMemo(
|
||||||
|
() =>
|
||||||
|
visibleDocumentIds
|
||||||
|
.map((id) => documentLookup.get(id) || null)
|
||||||
|
.filter((doc): doc is DocumentLike => Boolean(doc)),
|
||||||
|
[visibleDocumentIds, documentLookup],
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleDocumentKeys = useMemo(
|
||||||
|
() => visibleDocumentIds.map((id) => resolveDocumentRowKey(id)).filter(Boolean),
|
||||||
|
[visibleDocumentIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleFolderKeys = useMemo(
|
||||||
|
() =>
|
||||||
|
showingSearchResults
|
||||||
|
? []
|
||||||
|
: currentSubfolders
|
||||||
|
.map((folder) => resolveFolderRowKey(folder.id))
|
||||||
|
.filter(Boolean),
|
||||||
|
[showingSearchResults, currentSubfolders],
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleRowKeys = useMemo(
|
||||||
|
() => [...visibleFolderKeys, ...visibleDocumentKeys],
|
||||||
|
[visibleFolderKeys, visibleDocumentKeys],
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleRowKeySet = useMemo(
|
||||||
|
() => new Set(visibleRowKeys),
|
||||||
|
[visibleRowKeys],
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
documentLinks,
|
documentLinks,
|
||||||
previewDocuments,
|
|
||||||
ensurePreviewData,
|
ensurePreviewData,
|
||||||
ensureDownloadUrl,
|
ensureDownloadUrl,
|
||||||
openDocumentPreview,
|
openDocumentPreview,
|
||||||
@@ -380,8 +452,7 @@ const useDocumentsWorkspace = ({
|
|||||||
removeDocumentLinks,
|
removeDocumentLinks,
|
||||||
} = useDocumentPreview({
|
} = useDocumentPreview({
|
||||||
routeDocumentId: previewDocumentId,
|
routeDocumentId: previewDocumentId,
|
||||||
documents,
|
documentsManager,
|
||||||
searchResults,
|
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
assetManager,
|
assetManager,
|
||||||
api,
|
api,
|
||||||
@@ -414,65 +485,6 @@ const useDocumentsWorkspace = ({
|
|||||||
const detailFolderFetchRef = useRef(new Set());
|
const detailFolderFetchRef = useRef(new Set());
|
||||||
|
|
||||||
|
|
||||||
const showingSearchResults = searchResults !== null;
|
|
||||||
|
|
||||||
const visibleDocuments = useMemo(
|
|
||||||
() => (showingSearchResults ? searchResults : documents),
|
|
||||||
[showingSearchResults, searchResults, documents],
|
|
||||||
);
|
|
||||||
|
|
||||||
const visibleDocumentIds = useMemo(
|
|
||||||
() => visibleDocuments.map((doc) => doc.id),
|
|
||||||
[visibleDocuments],
|
|
||||||
);
|
|
||||||
|
|
||||||
const visibleDocumentKeys = useMemo(
|
|
||||||
() => visibleDocumentIds.map((id) => resolveDocumentRowKey(id)).filter(Boolean),
|
|
||||||
[visibleDocumentIds],
|
|
||||||
);
|
|
||||||
|
|
||||||
const visibleFolderKeys = useMemo(
|
|
||||||
() =>
|
|
||||||
showingSearchResults
|
|
||||||
? []
|
|
||||||
: currentSubfolders
|
|
||||||
.map((folder) => resolveFolderRowKey(folder.id))
|
|
||||||
.filter(Boolean),
|
|
||||||
[showingSearchResults, currentSubfolders],
|
|
||||||
);
|
|
||||||
|
|
||||||
const visibleRowKeys = useMemo(
|
|
||||||
() => [...visibleFolderKeys, ...visibleDocumentKeys],
|
|
||||||
[visibleFolderKeys, visibleDocumentKeys],
|
|
||||||
);
|
|
||||||
|
|
||||||
const visibleRowKeySet = useMemo(
|
|
||||||
() => new Set(visibleRowKeys),
|
|
||||||
[visibleRowKeys],
|
|
||||||
);
|
|
||||||
|
|
||||||
const documentLookup = useMemo(() => {
|
|
||||||
const map = new Map();
|
|
||||||
const push = (items) => {
|
|
||||||
(items || []).forEach((doc) => {
|
|
||||||
if (doc?.id) {
|
|
||||||
map.set(doc.id, doc);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
push(documents);
|
|
||||||
if (Array.isArray(searchResults)) {
|
|
||||||
push(searchResults);
|
|
||||||
}
|
|
||||||
previewDocuments.forEach((doc, id) => {
|
|
||||||
if (doc && id && !map.has(id)) {
|
|
||||||
map.set(id, doc);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return map;
|
|
||||||
}, [documents, searchResults, previewDocuments]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showingSearchResults) {
|
if (!showingSearchResults) {
|
||||||
return;
|
return;
|
||||||
@@ -696,7 +708,7 @@ const useDocumentsWorkspace = ({
|
|||||||
selectionAnchorRef.current = null;
|
selectionAnchorRef.current = null;
|
||||||
setDraggedDocumentIds([]);
|
setDraggedDocumentIds([]);
|
||||||
setDraggedFolderId(null);
|
setDraggedFolderId(null);
|
||||||
setSearchResults(null);
|
setSearchResultIds(null);
|
||||||
setTags([]);
|
setTags([]);
|
||||||
setCorrespondents([]);
|
setCorrespondents([]);
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
@@ -729,7 +741,7 @@ const useDocumentsWorkspace = ({
|
|||||||
setDocuments,
|
setDocuments,
|
||||||
setDraggedDocumentIds,
|
setDraggedDocumentIds,
|
||||||
setDraggedFolderId,
|
setDraggedFolderId,
|
||||||
setSearchResults,
|
setSearchResultIds,
|
||||||
setTags,
|
setTags,
|
||||||
setCorrespondents,
|
setCorrespondents,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
@@ -757,11 +769,11 @@ const useDocumentsWorkspace = ({
|
|||||||
const idSet = new Set<DocumentId>(documentIds);
|
const idSet = new Set<DocumentId>(documentIds);
|
||||||
|
|
||||||
setDocuments((prev) => prev.filter((doc) => !idSet.has(doc.id)));
|
setDocuments((prev) => prev.filter((doc) => !idSet.has(doc.id)));
|
||||||
setSearchResults((prev) => {
|
setSearchResultIds((prev) => {
|
||||||
if (!Array.isArray(prev)) {
|
if (!Array.isArray(prev)) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
const filtered = prev.filter((doc) => !idSet.has(doc.id));
|
const filtered = prev.filter((id) => !idSet.has(id as DocumentId));
|
||||||
return filtered.length === prev.length ? prev : filtered;
|
return filtered.length === prev.length ? prev : filtered;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -788,9 +800,16 @@ const useDocumentsWorkspace = ({
|
|||||||
return changed ? next : prev;
|
return changed ? next : prev;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
removeDocumentsFromLookup(Array.from(idSet));
|
||||||
removeDocumentLinks(Array.from(idSet));
|
removeDocumentLinks(Array.from(idSet));
|
||||||
},
|
},
|
||||||
[setDocuments, setSearchResults, setFolderContents, removeDocumentLinks],
|
[
|
||||||
|
setDocuments,
|
||||||
|
setSearchResultIds,
|
||||||
|
setFolderContents,
|
||||||
|
removeDocumentsFromLookup,
|
||||||
|
removeDocumentLinks,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -812,7 +831,7 @@ const useDocumentsWorkspace = ({
|
|||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
setDocuments,
|
setDocuments,
|
||||||
setFolderContents,
|
setFolderContents,
|
||||||
setSearchResults,
|
setSearchResultIds,
|
||||||
setSelectedEntries,
|
setSelectedEntries,
|
||||||
setSelectionOrder,
|
setSelectionOrder,
|
||||||
selectionOrderRef,
|
selectionOrderRef,
|
||||||
@@ -864,7 +883,7 @@ const useDocumentsWorkspace = ({
|
|||||||
setLoading,
|
setLoading,
|
||||||
setFolderContents,
|
setFolderContents,
|
||||||
setCurrentFolder,
|
setCurrentFolder,
|
||||||
setSearchResults,
|
setSearchResultIds,
|
||||||
isFilterActive,
|
isFilterActive,
|
||||||
navigate,
|
navigate,
|
||||||
handleFileDrop,
|
handleFileDrop,
|
||||||
@@ -883,7 +902,7 @@ const useDocumentsWorkspace = ({
|
|||||||
} = useDocumentsSelection({
|
} = useDocumentsSelection({
|
||||||
showingSearchResults,
|
showingSearchResults,
|
||||||
currentSubfolders,
|
currentSubfolders,
|
||||||
visibleDocuments,
|
visibleDocuments: viewDocuments,
|
||||||
resolveFolderRowKey,
|
resolveFolderRowKey,
|
||||||
resolveDocumentRowKey,
|
resolveDocumentRowKey,
|
||||||
configureSelectionEnvironment,
|
configureSelectionEnvironment,
|
||||||
@@ -1021,19 +1040,13 @@ const useDocumentsWorkspace = ({
|
|||||||
prev.map((doc) => (doc.id === documentId ? mergeAssetIntoDocument(doc, entry) : doc)),
|
prev.map((doc) => (doc.id === documentId ? mergeAssetIntoDocument(doc, entry) : doc)),
|
||||||
);
|
);
|
||||||
|
|
||||||
setSearchResults((prev) =>
|
|
||||||
Array.isArray(prev)
|
|
||||||
? prev.map((doc) => (doc.id === documentId ? mergeAssetIntoDocument(doc, entry) : doc))
|
|
||||||
: prev,
|
|
||||||
);
|
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifyApiError(error, 'Unable to refresh document asset.');
|
notifyApiError(error, 'Unable to refresh document asset.');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[assetManager, setDocuments, setSearchResults, notifyApiError],
|
[assetManager, setDocuments, notifyApiError],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
@@ -1233,9 +1246,7 @@ const useDocumentsWorkspace = ({
|
|||||||
documentLink,
|
documentLink,
|
||||||
resolveFolderPath,
|
resolveFolderPath,
|
||||||
} = useDetailWorkspace({
|
} = useDetailWorkspace({
|
||||||
documents,
|
documents: viewDocuments,
|
||||||
searchResults,
|
|
||||||
previewDocuments,
|
|
||||||
selectionOrder,
|
selectionOrder,
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
documentLookup,
|
documentLookup,
|
||||||
@@ -1446,8 +1457,7 @@ const useDocumentsWorkspace = ({
|
|||||||
|
|
||||||
const deskWorkspaceProps = useMemo(
|
const deskWorkspaceProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
documents,
|
documents: viewDocuments,
|
||||||
searchResults,
|
|
||||||
onInspectDocument: inspectDocumentForDesk,
|
onInspectDocument: inspectDocumentForDesk,
|
||||||
onEntryPointer: handleEntryPointerCore,
|
onEntryPointer: handleEntryPointerCore,
|
||||||
onDocumentStackSelect: handleDeskDocumentStackSelect,
|
onDocumentStackSelect: handleDeskDocumentStackSelect,
|
||||||
@@ -1464,8 +1474,7 @@ const useDocumentsWorkspace = ({
|
|||||||
ensureDownloadUrl,
|
ensureDownloadUrl,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
documents,
|
viewDocuments,
|
||||||
searchResults,
|
|
||||||
inspectDocumentForDesk,
|
inspectDocumentForDesk,
|
||||||
handleEntryPointerCore,
|
handleEntryPointerCore,
|
||||||
handleDeskDocumentStackSelect,
|
handleDeskDocumentStackSelect,
|
||||||
@@ -1488,8 +1497,8 @@ const useDocumentsWorkspace = ({
|
|||||||
breadcrumbs,
|
breadcrumbs,
|
||||||
refreshCurrentFolder,
|
refreshCurrentFolder,
|
||||||
currentSubfolders,
|
currentSubfolders,
|
||||||
documents,
|
documents: viewDocuments,
|
||||||
searchResults,
|
searchResultIds,
|
||||||
folderClickHandlers,
|
folderClickHandlers,
|
||||||
handleFolderDragStart,
|
handleFolderDragStart,
|
||||||
handleFolderDragEnd,
|
handleFolderDragEnd,
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ interface UseFolderTreeActionsOptions {
|
|||||||
updater: (prev: Map<FolderKey, FolderContentsState>) => Map<FolderKey, FolderContentsState>,
|
updater: (prev: Map<FolderKey, FolderContentsState>) => Map<FolderKey, FolderContentsState>,
|
||||||
) => void;
|
) => void;
|
||||||
setCurrentFolder: (updater: (prev: any) => any) => void;
|
setCurrentFolder: (updater: (prev: any) => any) => void;
|
||||||
setSearchResults: (value: any) => void;
|
setSearchResultIds: (value: any) => void;
|
||||||
isFilterActive: boolean;
|
isFilterActive: boolean;
|
||||||
navigate?: (path: string, options?: { replace?: boolean }) => void;
|
navigate?: (path: string, options?: { replace?: boolean }) => void;
|
||||||
handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderKey) => Promise<void> | void;
|
handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderKey) => Promise<void> | void;
|
||||||
@@ -99,7 +99,7 @@ const useFolderTreeActions = ({
|
|||||||
setLoading,
|
setLoading,
|
||||||
setFolderContents,
|
setFolderContents,
|
||||||
setCurrentFolder,
|
setCurrentFolder,
|
||||||
setSearchResults,
|
setSearchResultIds,
|
||||||
isFilterActive,
|
isFilterActive,
|
||||||
navigate,
|
navigate,
|
||||||
handleFileDrop,
|
handleFileDrop,
|
||||||
@@ -235,7 +235,7 @@ const useFolderTreeActions = ({
|
|||||||
}
|
}
|
||||||
applySelectedFolder(targetId, contents);
|
applySelectedFolder(targetId, contents);
|
||||||
if (!preserveSearch) {
|
if (!preserveSearch) {
|
||||||
setSearchResults(null);
|
setSearchResultIds(null);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifyApiError(error, 'Failed to load folder contents.');
|
notifyApiError(error, 'Failed to load folder contents.');
|
||||||
@@ -250,7 +250,7 @@ const useFolderTreeActions = ({
|
|||||||
expandFolderAncestors,
|
expandFolderAncestors,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
setLoading,
|
setLoading,
|
||||||
setSearchResults,
|
setSearchResultIds,
|
||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -253,12 +253,35 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
|
|
||||||
const [zoomOverlayOpen, setZoomOverlayOpen] = useState(false);
|
const [zoomOverlayOpen, setZoomOverlayOpen] = useState(false);
|
||||||
|
|
||||||
|
const fallbackDocumentLink = useMemo(() => {
|
||||||
|
if (!document) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const downloadPath = document.current_version?.download_path;
|
||||||
|
if (!downloadPath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const href = resolveApiPath ? resolveApiPath(downloadPath) : downloadPath;
|
||||||
|
if (!href) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const contentType = document.current_version?.version?.content_type || document.content_type || null;
|
||||||
|
const filename = document.current_version?.filename || document.filename || document.title || null;
|
||||||
|
return {
|
||||||
|
url: href,
|
||||||
|
contentType,
|
||||||
|
filename,
|
||||||
|
};
|
||||||
|
}, [document, resolveApiPath]);
|
||||||
|
|
||||||
|
const effectiveDocumentLink = documentLink?.url ? documentLink : fallbackDocumentLink;
|
||||||
|
|
||||||
const handleZoomOpen = useCallback(() => {
|
const handleZoomOpen = useCallback(() => {
|
||||||
if (!documentLink?.url) {
|
if (!effectiveDocumentLink?.url) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setZoomOverlayOpen(true);
|
setZoomOverlayOpen(true);
|
||||||
}, [documentLink?.url]);
|
}, [effectiveDocumentLink?.url]);
|
||||||
|
|
||||||
const handleZoomClose = useCallback(() => {
|
const handleZoomClose = useCallback(() => {
|
||||||
setZoomOverlayOpen(false);
|
setZoomOverlayOpen(false);
|
||||||
@@ -266,7 +289,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setZoomOverlayOpen(false);
|
setZoomOverlayOpen(false);
|
||||||
}, [documentLink?.url, document?.id]);
|
}, [effectiveDocumentLink?.url, document?.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hydrateDocument && document?.id) {
|
if (hydrateDocument && document?.id) {
|
||||||
@@ -355,23 +378,23 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
}, [breadcrumbs, handleBreadcrumbNavigate]);
|
}, [breadcrumbs, handleBreadcrumbNavigate]);
|
||||||
|
|
||||||
const zoomDisplay = useMemo(() => {
|
const zoomDisplay = useMemo(() => {
|
||||||
if (!documentLink?.url || !document) {
|
if (!effectiveDocumentLink?.url || !document) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const docContentType = document.content_type;
|
const docContentType = document.content_type;
|
||||||
const versionContentType = document.current_version?.version?.content_type;
|
const versionContentType = document.current_version?.version?.content_type;
|
||||||
const normalizedContentType = documentLink.contentType || docContentType || versionContentType || null;
|
const normalizedContentType = effectiveDocumentLink.contentType || docContentType || versionContentType || null;
|
||||||
return {
|
return {
|
||||||
url: documentLink.url,
|
url: effectiveDocumentLink.url,
|
||||||
alt: document.title,
|
alt: document.title,
|
||||||
contentType: normalizedContentType || undefined,
|
contentType: normalizedContentType || undefined,
|
||||||
};
|
};
|
||||||
}, [documentLink?.url, documentLink?.contentType, document]);
|
}, [effectiveDocumentLink?.url, effectiveDocumentLink?.contentType, document]);
|
||||||
|
|
||||||
const headerActions = createDocumentViewerHeaderActions({
|
const headerActions = createDocumentViewerHeaderActions({
|
||||||
document,
|
document,
|
||||||
actionState,
|
actionState,
|
||||||
documentLink,
|
documentLink: effectiveDocumentLink,
|
||||||
onZoom: zoomDisplay ? handleZoomOpen : null,
|
onZoom: zoomDisplay ? handleZoomOpen : null,
|
||||||
canZoom: Boolean(zoomDisplay),
|
canZoom: Boolean(zoomDisplay),
|
||||||
});
|
});
|
||||||
@@ -471,7 +494,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
<section className={viewerClassName}>
|
<section className={viewerClassName}>
|
||||||
<DocumentViewerLayout
|
<DocumentViewerLayout
|
||||||
document={document}
|
document={document}
|
||||||
documentLink={documentLink}
|
documentLink={effectiveDocumentLink}
|
||||||
summaryProps={summaryProps}
|
summaryProps={summaryProps}
|
||||||
metadataPayload={metadataPayload}
|
metadataPayload={metadataPayload}
|
||||||
contentTabConfig={contentTabConfig}
|
contentTabConfig={contentTabConfig}
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ const DocumentViewerRoute: React.FC = () => {
|
|||||||
return <Navigate to="/documents" replace />;
|
return <Navigate to="/documents" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!previewWorkspaceDocument || previewWorkspaceDocument.id !== documentId) {
|
const previewId = previewWorkspaceDocument?.id;
|
||||||
|
if (!previewWorkspaceDocument || String(previewId) !== String(documentId)) {
|
||||||
return <div className="document-viewer__message">Loading preview…</div>;
|
return <div className="document-viewer__message">Loading preview…</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user