1578 lines
40 KiB
JavaScript
1578 lines
40 KiB
JavaScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import {
|
|
matchPath,
|
|
useLocation,
|
|
useMatch,
|
|
useNavigate,
|
|
} from 'react-router-dom';
|
|
import AssetManager, { getAssetFromVersion } from '../../asset_manager';
|
|
import useApiError from '../useApiError';
|
|
import TagManager from '../../tag_manager';
|
|
import usePasskeys from '../../settings/usePasskeys';
|
|
import { useManagementModals } from '../../app/useManagementModals';
|
|
import { api, useAppDispatch, useAppState } from '../../app/appState';
|
|
import useWorkspaceSelection from '../../app/useWorkspaceSelection';
|
|
import { useEntryPointer as useEntryPointerCore } from '../../documents/useEntryPointer';
|
|
import { isTagTransferEvent } from '../../documents/tagTransfer';
|
|
import useDocumentsSelection from '../../documents/hooks/useDocumentsSelection';
|
|
import useBulkDocumentActions from '../../documents/hooks/useBulkDocumentActions';
|
|
import useDocumentsPanelProps from '../../documents/hooks/useDocumentsPanelProps';
|
|
import useDocumentPreview from '../../app/useDocumentPreview';
|
|
import useDeskWorkspaceProps from '../../desktop/useDeskWorkspaceProps';
|
|
import useSidebarProps from '../../sidebar/useSidebarProps';
|
|
import {
|
|
ASSET_PRESIGN_TTL_MS,
|
|
DEFAULT_FOLDER_NAME,
|
|
DEFAULT_SORT_DIRECTION,
|
|
DEFAULT_SORT_FIELD,
|
|
createRootNode,
|
|
getRowId,
|
|
isDocumentRowKey,
|
|
isFolderRowKey,
|
|
mergeAssetIntoDocument,
|
|
resolveApiPath,
|
|
resolveDocumentRowKey,
|
|
resolveFolderRowKey,
|
|
} from '../../app/appLayoutUtils';
|
|
import useDocumentsSearch from '../../app/useDocumentsSearch';
|
|
import useDocumentsStore from './store/useDocumentsStore';
|
|
import useAuthManager from './useAuthManager';
|
|
import useTags from './useTags';
|
|
import useCorrespondents from './useCorrespondents';
|
|
import useTenantManager from './useTenantManager';
|
|
import useDocuments from './useDocuments';
|
|
import useFolderTree from './useFolderTree';
|
|
import useFolderTreeActions from './useFolderTreeActions';
|
|
import useDocumentTagging from './useDocumentTagging';
|
|
import useDocumentCorrespondentActions from './useDocumentCorrespondentActions';
|
|
import useDocumentUploads from './useDocumentUploads';
|
|
import useDocumentDragHandlers from './useDocumentDragHandlers';
|
|
import useDocumentMutations from './useDocumentMutations';
|
|
import useDetailWorkspace from '../../detail/useDetailWorkspace';
|
|
|
|
const EntryType = Object.freeze({
|
|
document: 'document',
|
|
folder: 'folder',
|
|
});
|
|
|
|
const noop = () => {};
|
|
|
|
const useDocumentsWorkspace = ({
|
|
documentsViewMode = 'list',
|
|
documentsSortField = DEFAULT_SORT_FIELD,
|
|
documentsSortDirection = DEFAULT_SORT_DIRECTION,
|
|
documentsSortFieldRef,
|
|
documentsSortDirectionRef,
|
|
onDocumentsViewModeChange,
|
|
onDocumentsSortFieldChange,
|
|
onDocumentsSortDirectionToggle,
|
|
searchIncludeDescendants = true,
|
|
onToggleSearchIncludeDescendants,
|
|
onSetSearchIncludeDescendants,
|
|
sortRefreshReadyRef,
|
|
handleDeskExit,
|
|
} = {}) => {
|
|
const handleDocumentsViewModeChange = onDocumentsViewModeChange || noop;
|
|
const handleDocumentsSortFieldChange = onDocumentsSortFieldChange || noop;
|
|
const handleDocumentsSortDirectionToggle = onDocumentsSortDirectionToggle || noop;
|
|
const toggleSearchIncludeDescendants = onToggleSearchIncludeDescendants || noop;
|
|
const setSearchIncludeDescendants = onSetSearchIncludeDescendants || noop;
|
|
const handleDeskExitSafe = handleDeskExit || noop;
|
|
|
|
const fallbackSortFieldRef = useRef(documentsSortField);
|
|
const activeSortFieldRef = documentsSortFieldRef || fallbackSortFieldRef;
|
|
useEffect(() => {
|
|
activeSortFieldRef.current = documentsSortField;
|
|
}, [documentsSortField, activeSortFieldRef]);
|
|
|
|
const fallbackSortDirectionRef = useRef(documentsSortDirection);
|
|
const activeSortDirectionRef = documentsSortDirectionRef || fallbackSortDirectionRef;
|
|
useEffect(() => {
|
|
activeSortDirectionRef.current = documentsSortDirection;
|
|
}, [documentsSortDirection, activeSortDirectionRef]);
|
|
|
|
const fallbackSortRefreshReadyRef = useRef(false);
|
|
const activeSortRefreshReadyRef = sortRefreshReadyRef || fallbackSortRefreshReadyRef;
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const appState = useAppState();
|
|
const appDispatch = useAppDispatch();
|
|
const folderMatch = matchPath('/documents/folder/:folderId', location.pathname);
|
|
const docMatch = matchPath('/documents/:documentId', location.pathname);
|
|
const routeFolderId = folderMatch?.params?.folderId || null;
|
|
const routeDocumentId = docMatch?.params?.documentId || null;
|
|
const previewDocumentId = routeDocumentId;
|
|
const { status: appStatus, token, tenant, tenants: tenantOptions = [] } = appState;
|
|
const tenantName = tenant?.name || tenant?.slug || null;
|
|
const currentTenantId = tenant?.id || null;
|
|
const { status, setStatusMessage } = useDocumentsStore();
|
|
const handleApiReport = useCallback(
|
|
({ message, variant }) => setStatusMessage(message, variant),
|
|
[setStatusMessage],
|
|
);
|
|
const reportApiError = useApiError({
|
|
onReport: handleApiReport,
|
|
});
|
|
const notifyApiError = useCallback(
|
|
(error, fallbackMessage, variant = 'error') =>
|
|
reportApiError(error, { message: fallbackMessage, variant }),
|
|
[reportApiError],
|
|
);
|
|
const [loading, setLoading] = useState(false);
|
|
const [creatingFolder, setCreatingFolder] = useState(false);
|
|
const { tokenRef, handleLogout } = useAuthManager({
|
|
apiClient: api,
|
|
token,
|
|
appStatus,
|
|
appDispatch,
|
|
notifyApiError,
|
|
setStatusMessage,
|
|
setLoading,
|
|
});
|
|
|
|
const breadcrumbFetchRef = useRef(new Set());
|
|
const tagRemovalCursorActiveRef = useRef(false);
|
|
const tenantIdRef = useRef(currentTenantId);
|
|
const detailPanelControlRef = useRef({ open: () => {}, close: () => {} });
|
|
const setTagRemovalCursor = useCallback((active) => {
|
|
if (typeof document === 'undefined') {
|
|
return;
|
|
}
|
|
if (tagRemovalCursorActiveRef.current === active) {
|
|
return;
|
|
}
|
|
const body = document.body;
|
|
if (!body) {
|
|
return;
|
|
}
|
|
tagRemovalCursorActiveRef.current = active;
|
|
if (active) {
|
|
body.classList.add('desk-cursor-remove');
|
|
} else {
|
|
body.classList.remove('desk-cursor-remove');
|
|
}
|
|
}, []);
|
|
const documentsRouteMatch = useMatch('/documents');
|
|
const documentsFolderRouteMatch = useMatch('/documents/folder/:folderId');
|
|
const documentsDetailRouteMatch = useMatch('/documents/:documentId');
|
|
const isDocumentsRoute = Boolean(
|
|
documentsRouteMatch || documentsFolderRouteMatch || documentsDetailRouteMatch,
|
|
);
|
|
|
|
const [draggedDocumentIds, setDraggedDocumentIds] = useState([]);
|
|
const [draggedFolderId, setDraggedFolderId] = useState(null);
|
|
const [activePreviewId, setActivePreviewId] = useState(routeDocumentId || null);
|
|
const shellRef = useRef(null);
|
|
const assetManagerRef = useRef(null);
|
|
if (!assetManagerRef.current) {
|
|
assetManagerRef.current = new AssetManager({ api, assetPresignTtlMs: ASSET_PRESIGN_TTL_MS });
|
|
}
|
|
const assetManager = assetManagerRef.current;
|
|
|
|
const extractDocumentFromResponse = useCallback(
|
|
(payload) => {
|
|
if (!payload) {
|
|
return null;
|
|
}
|
|
const hydratedDetail = assetManager.hydrateDetail(payload);
|
|
return hydratedDetail?.document || payload.document || payload;
|
|
},
|
|
[assetManager],
|
|
);
|
|
|
|
const tagManagerRef = useRef(null);
|
|
if (!tagManagerRef.current) {
|
|
tagManagerRef.current = new TagManager();
|
|
}
|
|
const tagManager = tagManagerRef.current;
|
|
|
|
const selection = useWorkspaceSelection({
|
|
resolveDocumentRowKey,
|
|
resolveFolderRowKey,
|
|
isDocumentRowKey,
|
|
isFolderRowKey,
|
|
getRowId,
|
|
});
|
|
|
|
const {
|
|
selectedEntries,
|
|
selectedDocumentIds,
|
|
selectedFolderIds,
|
|
setSelectedEntries,
|
|
selectionOrder,
|
|
setSelectionOrder,
|
|
selectionOrderRef,
|
|
selectionAnchorRef,
|
|
selectionInitializedRef,
|
|
focusedDocumentId,
|
|
setFocusedDocumentId,
|
|
focusedRowKey,
|
|
setFocusedRowKey,
|
|
applySelection,
|
|
clearSelection,
|
|
handleEntrySelection,
|
|
promoteSelectionOrder: promoteSelectionOrderRaw,
|
|
configureSelectionEnvironment,
|
|
} = selection;
|
|
|
|
const selectionHelpers = useMemo(
|
|
() => ({
|
|
setSelectedEntries,
|
|
setSelectionOrder,
|
|
selectionOrderRef,
|
|
selectionAnchorRef,
|
|
setFocusedDocumentId,
|
|
focusedDocumentId,
|
|
selectionInitializedRef,
|
|
}),
|
|
[
|
|
setSelectedEntries,
|
|
setSelectionOrder,
|
|
selectionOrderRef,
|
|
selectionAnchorRef,
|
|
setFocusedDocumentId,
|
|
focusedDocumentId,
|
|
selectionInitializedRef,
|
|
],
|
|
);
|
|
|
|
const [folderContents, setFolderContents] = useState(() => new Map());
|
|
const folderContentsRef = useRef(folderContents);
|
|
useEffect(() => {
|
|
folderContentsRef.current = folderContents;
|
|
}, [folderContents]);
|
|
|
|
const setSearchResultsRef = useRef(() => {});
|
|
const setSearchResultsProxy = useCallback((value) => {
|
|
if (typeof setSearchResultsRef.current === 'function') {
|
|
setSearchResultsRef.current(value);
|
|
}
|
|
}, []);
|
|
|
|
const {
|
|
documents,
|
|
setDocuments,
|
|
mapDocumentCaches,
|
|
updateDocumentCaches,
|
|
} = useDocuments({
|
|
setSearchResults: setSearchResultsProxy,
|
|
setFolderContents,
|
|
});
|
|
|
|
const {
|
|
folderNodes,
|
|
setFolderNodes,
|
|
selectedFolder,
|
|
setSelectedFolder,
|
|
currentFolder,
|
|
setCurrentFolder,
|
|
currentSubfolders,
|
|
setCurrentSubfolders,
|
|
currentFolderName,
|
|
folderOptions,
|
|
folderLabelMap,
|
|
applySelectedFolder,
|
|
ensureFolderData,
|
|
ensureFolderAncestorsLoaded,
|
|
expandFolderAncestors,
|
|
isInvalidFolderDrop,
|
|
} = useFolderTree({
|
|
initialSelectedFolder: routeFolderId || 'root',
|
|
assetManager,
|
|
apiClient: api,
|
|
tenantIdRef,
|
|
documentsSortFieldRef: activeSortFieldRef,
|
|
documentsSortDirectionRef: activeSortDirectionRef,
|
|
selectionHelpers,
|
|
setDocuments,
|
|
setFolderContents,
|
|
folderContentsRef,
|
|
});
|
|
|
|
const {
|
|
searchQuery,
|
|
setSearchQuery,
|
|
searchResults,
|
|
setSearchResults,
|
|
searchLoading,
|
|
activeTagFilters,
|
|
setActiveTagFilters,
|
|
activeCorrespondentFilters,
|
|
setActiveCorrespondentFilters,
|
|
toggleTagFilter,
|
|
toggleCorrespondentFilter,
|
|
isFilterActive,
|
|
clearFilters,
|
|
handleSearchChange,
|
|
handleSearchSubmit,
|
|
} = useDocumentsSearch({
|
|
api,
|
|
assetManager,
|
|
token,
|
|
selectedFolder,
|
|
navigate,
|
|
locationPathname: location.pathname,
|
|
isDocumentsRoute,
|
|
selectionHelpers,
|
|
searchIncludeDescendants,
|
|
documentsSortField,
|
|
documentsSortDirection,
|
|
notifyApiError,
|
|
setLoading,
|
|
setSearchIncludeDescendants,
|
|
});
|
|
|
|
useEffect(() => {
|
|
setSearchResultsRef.current = setSearchResults;
|
|
}, [setSearchResults]);
|
|
|
|
const {
|
|
previewEntries,
|
|
previewDocuments,
|
|
ensurePreviewData,
|
|
openDocumentPreview,
|
|
closeDocumentPreview,
|
|
resetPreviewState,
|
|
removePreviewEntries,
|
|
} = useDocumentPreview({
|
|
routeDocumentId: previewDocumentId,
|
|
documents,
|
|
searchResults,
|
|
selectedFolder,
|
|
assetManager,
|
|
api,
|
|
resolveApiPath,
|
|
notifyApiError,
|
|
navigate,
|
|
locationPathname: location.pathname,
|
|
locationSearch: location.search,
|
|
detailPanelControlRef,
|
|
setActivePreviewId,
|
|
});
|
|
|
|
const getDocumentAsset = useCallback((doc, type) => {
|
|
if (!doc || !type) return null;
|
|
return getAssetFromVersion(doc.current_version || null, type);
|
|
}, []);
|
|
|
|
const bootstrapInitializedRef = useRef(false);
|
|
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]);
|
|
|
|
const {
|
|
tags,
|
|
refreshTags,
|
|
handleTagCreate,
|
|
handleTagUpdate,
|
|
handleTagDelete,
|
|
setTags,
|
|
} = useTags({
|
|
apiClient: api,
|
|
notifyApiError,
|
|
setStatusMessage,
|
|
tagManager,
|
|
tenantIdRef,
|
|
setActiveTagFilters,
|
|
mapDocumentCaches,
|
|
});
|
|
|
|
|
|
useEffect(() => {
|
|
tenantIdRef.current = currentTenantId;
|
|
}, [currentTenantId]);
|
|
|
|
|
|
useEffect(() => {
|
|
if (!selectedDocumentIds.length) {
|
|
return;
|
|
}
|
|
if (!selectedDocumentIds.includes(activePreviewId)) {
|
|
setActivePreviewId(selectedDocumentIds[selectedDocumentIds.length - 1]);
|
|
}
|
|
selectionInitializedRef.current = true;
|
|
}, [selectedDocumentIds, activePreviewId, selectionInitializedRef]);
|
|
|
|
|
|
const tagLookupById = useMemo(() => {
|
|
const map = new Map();
|
|
tags.forEach((tag) => {
|
|
if (tag?.id) {
|
|
map.set(tag.id, tag);
|
|
}
|
|
});
|
|
return map;
|
|
}, [tags]);
|
|
|
|
const {
|
|
correspondents,
|
|
refreshCorrespondents,
|
|
handleCorrespondentCreate,
|
|
handleCorrespondentUpdate,
|
|
handleCorrespondentDelete,
|
|
setCorrespondents,
|
|
} = useCorrespondents({
|
|
apiClient: api,
|
|
notifyApiError,
|
|
setStatusMessage,
|
|
tenantIdRef,
|
|
mapDocumentCaches,
|
|
});
|
|
|
|
const {
|
|
passkeys,
|
|
passkeysSupported,
|
|
passkeysLoading,
|
|
registeringPasskey,
|
|
revokingPasskeyId,
|
|
refreshPasskeys,
|
|
registerPasskey,
|
|
revokePasskey,
|
|
} = usePasskeys({
|
|
api,
|
|
notifyApiError,
|
|
setStatusMessage,
|
|
token,
|
|
});
|
|
|
|
const resolveTargetDocumentIds = useCallback(
|
|
(candidateIds) => {
|
|
const normalized = Array.isArray(candidateIds)
|
|
? candidateIds.filter(Boolean)
|
|
: [];
|
|
if (normalized.length) {
|
|
return Array.from(new Set(normalized));
|
|
}
|
|
return selectedDocumentIds;
|
|
},
|
|
[selectedDocumentIds],
|
|
);
|
|
|
|
const refreshCurrentFolder = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const contents = await ensureFolderData(selectedFolder, {
|
|
force: true,
|
|
prefetchDepth: 1,
|
|
});
|
|
applySelectedFolder(selectedFolder, contents);
|
|
} catch (error) {
|
|
notifyApiError(error, 'Failed to refresh folder.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [selectedFolder, ensureFolderData, applySelectedFolder, notifyApiError]);
|
|
|
|
const {
|
|
handleBulkTagAddFromDetail,
|
|
handleBulkTagRemoveFromDetail,
|
|
handleBulkSelectionReanalyze,
|
|
} = useDocumentTagging({
|
|
apiClient: api,
|
|
tags,
|
|
tagManager,
|
|
refreshTags,
|
|
refreshCurrentFolder,
|
|
resolveTargetDocumentIds,
|
|
notifyApiError,
|
|
setStatusMessage,
|
|
setLoading,
|
|
});
|
|
|
|
const {
|
|
dropOverlayState,
|
|
handleFileDrop,
|
|
handleFileSelection,
|
|
uploadQueue,
|
|
clearUploadQueue,
|
|
resetUploadsState,
|
|
} = useDocumentUploads({
|
|
apiClient: api,
|
|
token,
|
|
selectedFolder,
|
|
currentFolderName,
|
|
ensureFolderData,
|
|
refreshCurrentFolder,
|
|
notifyApiError,
|
|
setStatusMessage,
|
|
setLoading,
|
|
shellRef,
|
|
});
|
|
|
|
const {
|
|
handleDocumentDragStart,
|
|
handleDocumentDragEnd,
|
|
handleFolderDragStart,
|
|
handleFolderDragEnd,
|
|
} = useDocumentDragHandlers({
|
|
selectedEntries,
|
|
selectedDocumentIds,
|
|
selectedFolderIds,
|
|
applySelection,
|
|
handleEntrySelection,
|
|
documentLookup,
|
|
setDraggedDocumentIds,
|
|
setDraggedFolderId,
|
|
resolveDocumentRowKey,
|
|
resolveFolderRowKey,
|
|
documentsViewMode,
|
|
});
|
|
|
|
const {
|
|
correspondentLookupByName,
|
|
handleDocumentCorrespondentAttach,
|
|
handleCorrespondentRemove,
|
|
handleCorrespondentAdd,
|
|
} = useDocumentCorrespondentActions({
|
|
apiClient: api,
|
|
correspondents,
|
|
handleCorrespondentCreate,
|
|
refreshCurrentFolder,
|
|
notifyApiError,
|
|
setStatusMessage,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!activeSortRefreshReadyRef.current) {
|
|
activeSortRefreshReadyRef.current = true;
|
|
return;
|
|
}
|
|
if (!isFilterActive && token) {
|
|
refreshCurrentFolder();
|
|
}
|
|
}, [
|
|
documentsSortField,
|
|
documentsSortDirection,
|
|
isFilterActive,
|
|
refreshCurrentFolder,
|
|
token,
|
|
activeSortRefreshReadyRef,
|
|
]);
|
|
|
|
const resetWorkspaceState = useCallback(() => {
|
|
const rootNode = createRootNode();
|
|
setFolderNodes(new Map([[rootNode.id, rootNode]]));
|
|
setFolderContents(new Map());
|
|
setSelectedFolder('root');
|
|
setCurrentFolder(null);
|
|
setCurrentSubfolders([]);
|
|
setDocuments([]);
|
|
setSelectedEntries([]);
|
|
setSelectionOrder([]);
|
|
selectionOrderRef.current = [];
|
|
setFocusedDocumentId(null);
|
|
selectionAnchorRef.current = null;
|
|
setDraggedDocumentIds([]);
|
|
setDraggedFolderId(null);
|
|
setSearchResults(null);
|
|
setTags([]);
|
|
setCorrespondents([]);
|
|
setSearchQuery('');
|
|
setActiveTagFilters([]);
|
|
setActiveCorrespondentFilters([]);
|
|
setActivePreviewId(null);
|
|
detailPanelControlRef.current.close();
|
|
assetManager.reset();
|
|
resetPreviewState();
|
|
resetUploadsState();
|
|
clearUploadQueue();
|
|
breadcrumbFetchRef.current = new Set();
|
|
detailFolderFetchRef.current = new Set();
|
|
bootstrapInitializedRef.current = false;
|
|
selectionInitializedRef.current = false;
|
|
tenantIdRef.current = null;
|
|
}, [
|
|
assetManager,
|
|
selectionAnchorRef,
|
|
selectionInitializedRef,
|
|
selectionOrderRef,
|
|
setFocusedDocumentId,
|
|
setSelectedEntries,
|
|
setSelectionOrder,
|
|
setFolderNodes,
|
|
setFolderContents,
|
|
setSelectedFolder,
|
|
setCurrentFolder,
|
|
setCurrentSubfolders,
|
|
setDocuments,
|
|
setDraggedDocumentIds,
|
|
setDraggedFolderId,
|
|
setSearchResults,
|
|
setTags,
|
|
setCorrespondents,
|
|
setSearchQuery,
|
|
setActiveTagFilters,
|
|
setActiveCorrespondentFilters,
|
|
setActivePreviewId,
|
|
resetPreviewState,
|
|
resetUploadsState,
|
|
clearUploadQueue,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (appStatus === 'logged-out' || appStatus === 'selecting-tenant') {
|
|
resetWorkspaceState();
|
|
}
|
|
}, [appStatus, resetWorkspaceState]);
|
|
|
|
|
|
const removeDocumentsFromCaches = useCallback(
|
|
(documentIds) => {
|
|
if (!documentIds || documentIds.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const idSet = new Set(documentIds);
|
|
|
|
setDocuments((prev) => prev.filter((doc) => !idSet.has(doc.id)));
|
|
setSearchResults((prev) => {
|
|
if (!Array.isArray(prev)) {
|
|
return prev;
|
|
}
|
|
const filtered = prev.filter((doc) => !idSet.has(doc.id));
|
|
return filtered.length === prev.length ? prev : filtered;
|
|
});
|
|
|
|
setFolderContents((prev) => {
|
|
if (!prev.size) {
|
|
return prev;
|
|
}
|
|
let changed = false;
|
|
const next = new Map();
|
|
prev.forEach((contents, key) => {
|
|
const docs = Array.isArray(contents?.documents) ? contents.documents : null;
|
|
if (!docs || docs.length === 0) {
|
|
next.set(key, contents);
|
|
return;
|
|
}
|
|
const filtered = docs.filter((doc) => !idSet.has(doc.id));
|
|
if (filtered.length !== docs.length) {
|
|
changed = true;
|
|
next.set(key, { ...contents, documents: filtered });
|
|
} else {
|
|
next.set(key, contents);
|
|
}
|
|
});
|
|
return changed ? next : prev;
|
|
});
|
|
|
|
removePreviewEntries(Array.from(idSet));
|
|
},
|
|
[setDocuments, setSearchResults, setFolderContents, removePreviewEntries],
|
|
);
|
|
|
|
const {
|
|
moveDocumentsToFolder,
|
|
handleThumbnailRegeneration,
|
|
handleDocumentsDelete,
|
|
handleDocumentTagAdd,
|
|
handleDocumentTagAttach,
|
|
handleDocumentTitleUpdate,
|
|
handleDocumentIssuedUpdate,
|
|
handleTagRemove,
|
|
} = useDocumentMutations({
|
|
api,
|
|
token,
|
|
documentLookup,
|
|
folderLabelMap,
|
|
ensureFolderData,
|
|
selectedFolder,
|
|
setSelectedFolder,
|
|
setDocuments,
|
|
setFolderContents,
|
|
setSearchResults,
|
|
setSelectedEntries,
|
|
setSelectionOrder,
|
|
selectionOrderRef,
|
|
selectionAnchorRef,
|
|
setFocusedDocumentId,
|
|
focusedDocumentId,
|
|
setFocusedRowKey,
|
|
focusedRowKey,
|
|
notifyApiError,
|
|
setStatusMessage,
|
|
setLoading,
|
|
mapDocumentCaches,
|
|
applySelectedFolder,
|
|
folderNodes,
|
|
setFolderNodes,
|
|
removeDocumentsFromCaches,
|
|
closeDocumentPreview,
|
|
previewDocumentId,
|
|
refreshCurrentFolder,
|
|
documentsViewMode,
|
|
updateDocumentCaches,
|
|
tagLookupById,
|
|
tags,
|
|
refreshTags,
|
|
tagManager,
|
|
extractDocumentFromResponse,
|
|
});
|
|
|
|
const {
|
|
loadFolder,
|
|
selectFolder,
|
|
handleFolderRename,
|
|
handleFolderCreate,
|
|
handleFolderDelete,
|
|
folderClickHandlers,
|
|
} = useFolderTreeActions({
|
|
api,
|
|
token,
|
|
folderNodes,
|
|
setFolderNodes,
|
|
selectedFolder,
|
|
setSelectedFolder,
|
|
ensureFolderData,
|
|
ensureFolderAncestorsLoaded,
|
|
expandFolderAncestors,
|
|
applySelectedFolder,
|
|
notifyApiError,
|
|
setStatusMessage,
|
|
setLoading,
|
|
setFolderContents,
|
|
setCurrentFolder,
|
|
setSearchResults,
|
|
isFilterActive,
|
|
navigate,
|
|
handleFileDrop,
|
|
moveDocumentsToFolder,
|
|
draggedDocumentIds,
|
|
draggedFolderId,
|
|
setDraggedDocumentIds,
|
|
setDraggedFolderId,
|
|
isInvalidFolderDrop,
|
|
setCreatingFolder,
|
|
});
|
|
|
|
const {
|
|
promoteSelectionOrder,
|
|
clearDocumentSelection,
|
|
} = useDocumentsSelection({
|
|
showingSearchResults,
|
|
currentSubfolders,
|
|
visibleDocuments,
|
|
resolveFolderRowKey,
|
|
resolveDocumentRowKey,
|
|
configureSelectionEnvironment,
|
|
visibleRowKeySet,
|
|
selectedEntries,
|
|
selectionAnchorRef,
|
|
promoteSelectionOrderRaw,
|
|
setFocusedDocumentId,
|
|
setActivePreviewId,
|
|
clearSelection,
|
|
focusedDocumentId,
|
|
setFocusedRowKey,
|
|
focusedRowKey,
|
|
isFolderRowKey,
|
|
});
|
|
const initializeAfterLogin = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
await Promise.all([refreshTags(), refreshCorrespondents()]);
|
|
const initialFolder = routeFolderId && routeFolderId !== 'root' ? routeFolderId : 'root';
|
|
await loadFolder(initialFolder, { showLoading: false });
|
|
} catch (error) {
|
|
notifyApiError(error, 'Failed to initialize data.');
|
|
throw error;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [refreshTags, refreshCorrespondents, routeFolderId, loadFolder, notifyApiError]);
|
|
|
|
useEffect(() => {
|
|
if (!token) {
|
|
return;
|
|
}
|
|
if (appStatus !== 'ready' && appStatus !== 'bootstrapping') {
|
|
return;
|
|
}
|
|
|
|
const targetParam = routeFolderId ?? 'root';
|
|
|
|
if (targetParam === 'root' && routeDocumentId) {
|
|
return;
|
|
}
|
|
|
|
const hasData = folderContents.has(targetParam);
|
|
if (targetParam !== selectedFolder || !hasData) {
|
|
selectFolder(targetParam, { immediate: true });
|
|
}
|
|
}, [
|
|
token,
|
|
appStatus,
|
|
routeFolderId,
|
|
routeDocumentId,
|
|
selectedFolder,
|
|
folderContents,
|
|
isFilterActive,
|
|
selectFolder,
|
|
]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
if (appStatus !== 'authenticated') {
|
|
return;
|
|
}
|
|
if (bootstrapInitializedRef.current) {
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
const bootstrap = async () => {
|
|
bootstrapInitializedRef.current = true;
|
|
appDispatch({ type: 'BOOTSTRAP_START' });
|
|
try {
|
|
await initializeAfterLogin();
|
|
if (!cancelled) {
|
|
appDispatch({ type: 'BOOTSTRAP_SUCCESS' });
|
|
}
|
|
} catch (error) {
|
|
if (!cancelled) {
|
|
appDispatch({
|
|
type: 'BOOTSTRAP_FAILURE',
|
|
error: error?.message || 'Failed to initialize data.',
|
|
});
|
|
bootstrapInitializedRef.current = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
bootstrap();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [appStatus, appDispatch, initializeAfterLogin]);
|
|
|
|
const {
|
|
handleBulkCorrespondentAdd,
|
|
handleBulkCorrespondentRemove,
|
|
handleDeleteSelection,
|
|
} = useBulkDocumentActions({
|
|
api,
|
|
resolveTargetDocumentIds,
|
|
correspondentLookupByName,
|
|
handleCorrespondentCreate,
|
|
refreshCurrentFolder,
|
|
setStatusMessage,
|
|
selectedDocumentIds,
|
|
selectedFolderIds,
|
|
handleDocumentsDelete,
|
|
handleFolderDelete,
|
|
clearDocumentSelection,
|
|
setLoading,
|
|
});
|
|
|
|
|
|
|
|
const ensureAssetUrl = useCallback(
|
|
async (documentId, asset, { force = false, start = null, limit = null } = {}) => {
|
|
if (!documentId || !asset?.id) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const entry = await assetManager.ensureAsset(documentId, asset, {
|
|
force,
|
|
start,
|
|
limit,
|
|
});
|
|
|
|
if (!entry) {
|
|
return null;
|
|
}
|
|
|
|
setDocuments((prev) =>
|
|
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;
|
|
} catch (error) {
|
|
notifyApiError(error, 'Unable to refresh document asset.');
|
|
throw error;
|
|
}
|
|
},
|
|
[assetManager, setDocuments, setSearchResults, notifyApiError],
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handleDocumentTagDrop = useCallback(
|
|
async (documentId, tag) => {
|
|
if (!documentId || !tag?.id) {
|
|
return;
|
|
}
|
|
|
|
if (tag.sourceDocId && tag.sourceDocId === documentId) {
|
|
return;
|
|
}
|
|
|
|
const attached = await handleDocumentTagAttach({ documentId, tagId: tag.id, tag });
|
|
if (!attached) {
|
|
return;
|
|
}
|
|
|
|
if (tag.sourceDocId && tag.sourceDocId !== documentId) {
|
|
await handleTagRemove(tag.sourceDocId, tag.id, {
|
|
refreshTagList: false,
|
|
showMessage: false,
|
|
});
|
|
}
|
|
},
|
|
[handleDocumentTagAttach, handleTagRemove],
|
|
);
|
|
|
|
|
|
|
|
const handlePromptCreateFolder = useCallback(async () => {
|
|
if (creatingFolder) {
|
|
return;
|
|
}
|
|
const input = window.prompt('New folder name');
|
|
if (!input) {
|
|
return;
|
|
}
|
|
const trimmed = input.trim();
|
|
if (!trimmed) {
|
|
setStatusMessage('Folder name cannot be empty.', 'error');
|
|
return;
|
|
}
|
|
setCreatingFolder(true);
|
|
try {
|
|
const success = await handleFolderCreate(trimmed);
|
|
if (!success) {
|
|
setStatusMessage('Unable to create folder. Check the status message for details.', 'error');
|
|
}
|
|
} finally {
|
|
setCreatingFolder(false);
|
|
}
|
|
}, [creatingFolder, handleFolderCreate, setStatusMessage]);
|
|
|
|
const { managementModals, openTagsModal, openCorrespondentsModal } = useManagementModals({
|
|
locationPathname: location.pathname,
|
|
tags,
|
|
refreshTags,
|
|
onTagCreate: handleTagCreate,
|
|
onTagUpdate: handleTagUpdate,
|
|
onTagDelete: handleTagDelete,
|
|
correspondents,
|
|
refreshCorrespondents,
|
|
onCorrespondentCreate: handleCorrespondentCreate,
|
|
onCorrespondentUpdate: handleCorrespondentUpdate,
|
|
onCorrespondentDelete: handleCorrespondentDelete,
|
|
setStatusMessage,
|
|
});
|
|
|
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
const openSettings = useCallback(() => {
|
|
setSettingsOpen(true);
|
|
}, []);
|
|
const closeSettings = useCallback(() => {
|
|
setSettingsOpen(false);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!settingsOpen) {
|
|
return undefined;
|
|
}
|
|
const handleKeyDown = (event) => {
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault();
|
|
setSettingsOpen(false);
|
|
}
|
|
};
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [settingsOpen]);
|
|
|
|
useEffect(
|
|
() => () => {
|
|
setTagRemovalCursor(false);
|
|
},
|
|
[setTagRemovalCursor],
|
|
);
|
|
|
|
useEffect(() => {
|
|
const host = shellRef.current;
|
|
if (!host) {
|
|
return undefined;
|
|
}
|
|
|
|
const isTagTransfer = (event) => isTagTransferEvent(event);
|
|
|
|
const isDocumentDropTarget = (target) =>
|
|
target instanceof Element ? Boolean(target.closest('[data-doc-id]')) : false;
|
|
|
|
const handleTagDragOver = (event) => {
|
|
if (!isTagTransfer(event)) {
|
|
return;
|
|
}
|
|
if (isDocumentDropTarget(event.target)) {
|
|
setTagRemovalCursor(false);
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
event.dataTransfer.dropEffect = 'move';
|
|
setTagRemovalCursor(true);
|
|
};
|
|
|
|
const handleTagDragLeave = (event) => {
|
|
if (!isTagTransfer(event)) {
|
|
return;
|
|
}
|
|
const related = event.relatedTarget;
|
|
if (related instanceof Element && host.contains(related)) {
|
|
if (isDocumentDropTarget(related)) {
|
|
setTagRemovalCursor(false);
|
|
}
|
|
return;
|
|
}
|
|
setTagRemovalCursor(false);
|
|
};
|
|
|
|
const handleTagDrop = async (event) => {
|
|
if (!isTagTransfer(event)) {
|
|
return;
|
|
}
|
|
setTagRemovalCursor(false);
|
|
if (isDocumentDropTarget(event.target) || event.defaultPrevented) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const raw =
|
|
event.dataTransfer.getData('application/x-papercrate-tag') ||
|
|
event.dataTransfer.getData('text/papercrate-tag');
|
|
if (!raw) {
|
|
return;
|
|
}
|
|
try {
|
|
const payload = JSON.parse(raw);
|
|
if (payload?.sourceDocId && payload?.id) {
|
|
await handleTagRemove(payload.sourceDocId, payload.id, {
|
|
refreshTagList: false,
|
|
showMessage: true,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to remove tag from drop target', error);
|
|
}
|
|
};
|
|
|
|
const handleTagDragEnd = () => {
|
|
setTagRemovalCursor(false);
|
|
};
|
|
|
|
host.addEventListener('dragover', handleTagDragOver, true);
|
|
host.addEventListener('dragleave', handleTagDragLeave, true);
|
|
host.addEventListener('drop', handleTagDrop, true);
|
|
window.addEventListener('dragend', handleTagDragEnd, true);
|
|
|
|
return () => {
|
|
host.removeEventListener('dragover', handleTagDragOver, true);
|
|
host.removeEventListener('dragleave', handleTagDragLeave, true);
|
|
host.removeEventListener('drop', handleTagDrop, true);
|
|
window.removeEventListener('dragend', handleTagDragEnd, true);
|
|
setTagRemovalCursor(false);
|
|
};
|
|
}, [handleTagRemove, setTagRemovalCursor]);
|
|
|
|
|
|
const {
|
|
detailPanelProps,
|
|
detailPanelOpen,
|
|
openDetailPanel,
|
|
handleDetailPanelClose,
|
|
inspectDocument,
|
|
previewActive,
|
|
previewWorkspaceDocument,
|
|
previewWorkspaceEntry,
|
|
resolveThumbnailUrlForDoc,
|
|
resolveFolderPath,
|
|
} = useDetailWorkspace({
|
|
documents,
|
|
searchResults,
|
|
previewDocuments,
|
|
focusedDocumentId,
|
|
selectionOrder,
|
|
selectedDocumentIds,
|
|
documentLookup,
|
|
folderNodes,
|
|
ensureFolderData,
|
|
detailPanelControlRef,
|
|
detailFolderFetchRef,
|
|
previewEntries,
|
|
previewDocumentId,
|
|
activePreviewId,
|
|
openDocumentPreview,
|
|
promoteSelectionOrder,
|
|
handleDocumentTitleUpdate,
|
|
handleDocumentIssuedUpdate,
|
|
handleDocumentTagAdd,
|
|
handleTagRemove,
|
|
ensureAssetUrl,
|
|
getDocumentAsset,
|
|
ensurePreviewData,
|
|
correspondents,
|
|
handleCorrespondentAdd,
|
|
handleCorrespondentRemove,
|
|
resolveApiPath,
|
|
selectFolder,
|
|
tags,
|
|
tagLookupById,
|
|
});
|
|
|
|
const handleEntryPointerCore = useEntryPointerCore({
|
|
resolveDocumentRowKey,
|
|
resolveFolderRowKey,
|
|
onSelectEntry: (entry, event, { rowKey, modifierClick, primaryClick }) => {
|
|
const { type, id } = entry;
|
|
const key = rowKey
|
|
|| (type === EntryType.document ? resolveDocumentRowKey(id) : resolveFolderRowKey(id));
|
|
if (key) {
|
|
handleEntrySelection(key, event);
|
|
}
|
|
if (type === EntryType.folder && !modifierClick && primaryClick) {
|
|
selectFolder(id);
|
|
}
|
|
},
|
|
});
|
|
|
|
const { breadcrumbs, missingBreadcrumbAncestors } = useMemo(() => {
|
|
const chain = [];
|
|
const seen = new Set();
|
|
const pending = new Set();
|
|
let currentId = selectedFolder || 'root';
|
|
let guard = 0;
|
|
|
|
while (currentId && !seen.has(currentId) && guard < 32) {
|
|
guard += 1;
|
|
seen.add(currentId);
|
|
|
|
if (currentId === 'root') {
|
|
chain.push({ id: 'root', name: DEFAULT_FOLDER_NAME });
|
|
currentId = null;
|
|
break;
|
|
}
|
|
|
|
const node = folderNodes.get(currentId);
|
|
if (node) {
|
|
chain.push({ id: currentId, name: node.name || 'Folder' });
|
|
currentId = node.parentId ?? 'root';
|
|
continue;
|
|
}
|
|
|
|
let fallbackName = '…';
|
|
let parentId = null;
|
|
|
|
if (currentFolder && currentFolder.id === currentId) {
|
|
fallbackName = currentFolder.name;
|
|
parentId = currentFolder.parent_id ?? 'root';
|
|
}
|
|
|
|
chain.push({ id: currentId, name: fallbackName });
|
|
pending.add(currentId);
|
|
currentId = parentId;
|
|
}
|
|
|
|
if (!chain.some((crumb) => crumb.id === 'root')) {
|
|
chain.push({ id: 'root', name: DEFAULT_FOLDER_NAME });
|
|
}
|
|
|
|
const ordered = [];
|
|
const seenOrdered = new Set();
|
|
chain
|
|
.slice()
|
|
.reverse()
|
|
.forEach((crumb) => {
|
|
if (!seenOrdered.has(crumb.id)) {
|
|
seenOrdered.add(crumb.id);
|
|
ordered.push(crumb);
|
|
}
|
|
});
|
|
|
|
return { breadcrumbs: ordered, missingBreadcrumbAncestors: Array.from(pending) };
|
|
}, [selectedFolder, folderNodes, currentFolder]);
|
|
|
|
useEffect(() => {
|
|
if (!missingBreadcrumbAncestors.length) {
|
|
return;
|
|
}
|
|
|
|
missingBreadcrumbAncestors.forEach((folderId) => {
|
|
if (!folderId || folderId === 'root') {
|
|
return;
|
|
}
|
|
if (breadcrumbFetchRef.current.has(folderId)) {
|
|
return;
|
|
}
|
|
|
|
breadcrumbFetchRef.current.add(folderId);
|
|
ensureFolderData(folderId, { force: false })
|
|
.catch((error) => {
|
|
console.warn('Failed to preload breadcrumb ancestor', folderId, error);
|
|
})
|
|
.finally(() => {
|
|
breadcrumbFetchRef.current.delete(folderId);
|
|
});
|
|
});
|
|
}, [missingBreadcrumbAncestors, ensureFolderData]);
|
|
|
|
const { handleTenantSelect } = useTenantManager({
|
|
apiClient: api,
|
|
appDispatch,
|
|
currentTenantId,
|
|
resetWorkspaceState,
|
|
setStatusMessage,
|
|
notifyApiError,
|
|
setLoading,
|
|
refreshTags,
|
|
refreshCorrespondents,
|
|
loadFolder,
|
|
handleDocumentsViewModeChange,
|
|
navigate,
|
|
tokenRef,
|
|
tenantIdRef,
|
|
});
|
|
|
|
|
|
const documentsTableProps = useDocumentsPanelProps({
|
|
currentFolderName,
|
|
breadcrumbs,
|
|
refreshCurrentFolder,
|
|
currentSubfolders,
|
|
documents,
|
|
searchResults,
|
|
isFilterActive,
|
|
folderClickHandlers,
|
|
handleFolderDragStart,
|
|
handleFolderDragEnd,
|
|
draggedFolderId,
|
|
handleFolderRename,
|
|
openDocumentPreview,
|
|
handleDocumentTitleUpdate,
|
|
selectedDocumentIds,
|
|
selectedFolderIds,
|
|
focusedRowKey,
|
|
draggedDocumentIds,
|
|
handleDocumentDragStart,
|
|
handleDocumentDragEnd,
|
|
searchLoading,
|
|
tagLookupById,
|
|
activeCorrespondentFilters,
|
|
selectedEntries,
|
|
setFocusedRowKey,
|
|
ensureAssetUrl,
|
|
getDocumentAsset,
|
|
toggleTagFilter,
|
|
toggleCorrespondentFilter,
|
|
handleDocumentTagDrop,
|
|
documentsViewMode,
|
|
documentsSortField,
|
|
documentsSortDirection,
|
|
handleDocumentsSortFieldChange,
|
|
handleDocumentsSortDirectionToggle,
|
|
searchIncludeDescendants,
|
|
toggleSearchIncludeDescendants,
|
|
handleDocumentsViewModeChange,
|
|
clearDocumentSelection,
|
|
handleDeleteSelection,
|
|
handleEntryPointerCore,
|
|
inspectDocument,
|
|
handleEntrySelection,
|
|
tags,
|
|
correspondents,
|
|
documentLookup,
|
|
handleBulkTagAddFromDetail,
|
|
handleBulkTagRemoveFromDetail,
|
|
handleBulkCorrespondentAdd,
|
|
handleBulkCorrespondentRemove,
|
|
handleBulkSelectionReanalyze,
|
|
folderOptions,
|
|
moveDocumentsToFolder,
|
|
selectFolder,
|
|
});
|
|
|
|
const sidebarProps = useSidebarProps({
|
|
folderNodes,
|
|
folderClickHandlers,
|
|
handleFolderDelete,
|
|
handleFolderRename,
|
|
selectedFolder,
|
|
handleFolderDragStart,
|
|
handleFolderDragEnd,
|
|
draggedFolderId,
|
|
handlePromptCreateFolder,
|
|
creatingFolder,
|
|
tags,
|
|
activeTagFilters,
|
|
toggleTagFilter,
|
|
handleTagCreate,
|
|
correspondents,
|
|
activeCorrespondentFilters,
|
|
toggleCorrespondentFilter,
|
|
handleCorrespondentCreate,
|
|
appStatus,
|
|
loading,
|
|
previewActive,
|
|
searchQuery,
|
|
handleSearchChange,
|
|
handleSearchSubmit,
|
|
clearFilters,
|
|
isFilterActive,
|
|
handleLogout,
|
|
status,
|
|
tenantName,
|
|
tenantOptions,
|
|
currentTenantId,
|
|
handleTenantSelect,
|
|
openSettings,
|
|
handleFileSelection,
|
|
uploadQueue,
|
|
});
|
|
|
|
|
|
|
|
const deskWorkspaceProps = useDeskWorkspaceProps({
|
|
documents,
|
|
searchResults,
|
|
breadcrumbs,
|
|
currentFolderName,
|
|
documentsViewMode,
|
|
handleDocumentsViewModeChange,
|
|
handleDeskExit: handleDeskExitSafe,
|
|
refreshCurrentFolder,
|
|
inspectDocument,
|
|
handleEntryPointerCore,
|
|
promoteSelectionOrder,
|
|
currentTenantId,
|
|
selectedDocumentIds,
|
|
selectedFolderIds,
|
|
clearDocumentSelection,
|
|
detailPanelOpen,
|
|
handleDetailPanelClose,
|
|
resolveThumbnailUrlForDoc,
|
|
handleDocumentTagDrop,
|
|
handleTagRemove,
|
|
ensureAssetUrl,
|
|
getDocumentAsset,
|
|
activeTagFilters,
|
|
handleDeleteSelection,
|
|
tags,
|
|
correspondents,
|
|
documentLookup,
|
|
tagLookupById,
|
|
handleBulkTagAddFromDetail,
|
|
handleBulkTagRemoveFromDetail,
|
|
handleBulkCorrespondentAdd,
|
|
handleBulkCorrespondentRemove,
|
|
handleBulkSelectionReanalyze,
|
|
folderOptions,
|
|
moveDocumentsToFolder,
|
|
searchIncludeDescendants,
|
|
toggleSearchIncludeDescendants,
|
|
selectedEntries,
|
|
selectionAnchorRef,
|
|
applySelection,
|
|
resolveDocumentRowKey,
|
|
showingSearchResults,
|
|
searchQuery,
|
|
activeCorrespondentFilters,
|
|
selectedFolder,
|
|
openDetailPanel,
|
|
});
|
|
|
|
const contextValue = useMemo(
|
|
() => ({
|
|
token,
|
|
appStatus,
|
|
status,
|
|
setStatusMessage,
|
|
dropOverlayState,
|
|
handleLogout,
|
|
sidebarProps,
|
|
tags,
|
|
refreshTags,
|
|
handleTagUpdate,
|
|
handleTagDelete,
|
|
handleDocumentTagDrop,
|
|
correspondents,
|
|
refreshCorrespondents,
|
|
handleCorrespondentUpdate,
|
|
handleCorrespondentCreate,
|
|
handleCorrespondentDelete,
|
|
handleDocumentCorrespondentAttach,
|
|
handleCorrespondentRemove,
|
|
handleCorrespondentAdd,
|
|
passkeys,
|
|
passkeysSupported,
|
|
passkeysLoading,
|
|
registeringPasskey,
|
|
revokingPasskeyId,
|
|
refreshPasskeys,
|
|
registerPasskey,
|
|
revokePasskey,
|
|
previewActive,
|
|
previewWorkspaceDocument,
|
|
previewWorkspaceEntry,
|
|
previewDocumentId,
|
|
closeDocumentPreview,
|
|
handleThumbnailRegeneration,
|
|
documentsTableProps,
|
|
detailPanelProps,
|
|
documentsViewMode,
|
|
deskWorkspaceProps,
|
|
ensurePreviewData,
|
|
ensureAssetUrl,
|
|
resolveFolderPath,
|
|
getDocumentAsset,
|
|
notifyApiError,
|
|
openTagsModal,
|
|
openCorrespondentsModal,
|
|
openSettings,
|
|
detailPanelOpen,
|
|
openDetailPanel,
|
|
uploadQueue,
|
|
clearUploadQueue,
|
|
}),
|
|
[
|
|
token,
|
|
appStatus,
|
|
status,
|
|
setStatusMessage,
|
|
dropOverlayState,
|
|
handleLogout,
|
|
sidebarProps,
|
|
tags,
|
|
refreshTags,
|
|
handleTagUpdate,
|
|
handleTagDelete,
|
|
handleDocumentTagDrop,
|
|
correspondents,
|
|
refreshCorrespondents,
|
|
handleCorrespondentUpdate,
|
|
handleCorrespondentCreate,
|
|
handleCorrespondentDelete,
|
|
handleDocumentCorrespondentAttach,
|
|
handleCorrespondentRemove,
|
|
handleCorrespondentAdd,
|
|
passkeys,
|
|
passkeysSupported,
|
|
passkeysLoading,
|
|
registeringPasskey,
|
|
revokingPasskeyId,
|
|
refreshPasskeys,
|
|
registerPasskey,
|
|
revokePasskey,
|
|
previewActive,
|
|
previewWorkspaceDocument,
|
|
previewWorkspaceEntry,
|
|
previewDocumentId,
|
|
closeDocumentPreview,
|
|
handleThumbnailRegeneration,
|
|
documentsTableProps,
|
|
detailPanelProps,
|
|
documentsViewMode,
|
|
deskWorkspaceProps,
|
|
ensurePreviewData,
|
|
ensureAssetUrl,
|
|
resolveFolderPath,
|
|
getDocumentAsset,
|
|
notifyApiError,
|
|
openTagsModal,
|
|
openCorrespondentsModal,
|
|
openSettings,
|
|
detailPanelOpen,
|
|
openDetailPanel,
|
|
uploadQueue,
|
|
clearUploadQueue,
|
|
],
|
|
);
|
|
|
|
// hook callers handle rendering / routing
|
|
return {
|
|
appStatus,
|
|
location,
|
|
shellRef,
|
|
dropOverlayState,
|
|
managementModals,
|
|
contextValue,
|
|
settingsOpen,
|
|
closeSettings,
|
|
};
|
|
};
|
|
|
|
|
|
export default useDocumentsWorkspace;
|