cleanup
This commit is contained in:
@@ -1,67 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Navigate, Outlet } from 'react-router-dom';
|
|
||||||
import { AppShellContext } from '../appShellContext';
|
|
||||||
import DropOverlay from './DropOverlay';
|
|
||||||
import UploadQueueOverlay from './UploadQueueOverlay';
|
|
||||||
import useDocumentsWorkspace from '../hooks/documents/useDocumentsWorkspace';
|
|
||||||
import { useDocumentsPreferences } from './useDocumentsPreferences';
|
|
||||||
import SettingsRoute from './SettingsRoute';
|
|
||||||
|
|
||||||
const AppLayout: React.FC = () => {
|
|
||||||
const documentsPreferences = useDocumentsPreferences();
|
|
||||||
const {
|
|
||||||
appStatus,
|
|
||||||
location,
|
|
||||||
shellRef,
|
|
||||||
dropOverlayState,
|
|
||||||
managementModals,
|
|
||||||
contextValue,
|
|
||||||
settingsOpen,
|
|
||||||
closeSettings,
|
|
||||||
} = useDocumentsWorkspace({
|
|
||||||
documentsViewMode: documentsPreferences.documentsViewMode,
|
|
||||||
documentsSortField: documentsPreferences.documentsSortField,
|
|
||||||
documentsSortDirection: documentsPreferences.documentsSortDirection,
|
|
||||||
documentsSortFieldRef: documentsPreferences.documentsSortFieldRef,
|
|
||||||
documentsSortDirectionRef: documentsPreferences.documentsSortDirectionRef,
|
|
||||||
onDocumentsViewModeChange: documentsPreferences.handleDocumentsViewModeChange,
|
|
||||||
onDocumentsSortFieldChange: documentsPreferences.handleDocumentsSortFieldChange,
|
|
||||||
onDocumentsSortDirectionToggle: documentsPreferences.handleDocumentsSortDirectionToggle,
|
|
||||||
searchIncludeDescendants: documentsPreferences.searchIncludeDescendants,
|
|
||||||
onSetSearchIncludeDescendants: documentsPreferences.setSearchIncludeDescendants,
|
|
||||||
sortRefreshReadyRef: documentsPreferences.sortRefreshReadyRef,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (['logged-out', 'authenticating', 'selecting-tenant'].includes(appStatus)) {
|
|
||||||
const redirectTarget = `${location.pathname}${location.search}${location.hash || ''}`;
|
|
||||||
return (
|
|
||||||
<Navigate
|
|
||||||
to="/account/login"
|
|
||||||
replace
|
|
||||||
state={{ from: redirectTarget }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppShellContext.Provider value={contextValue}>
|
|
||||||
<div className="app-shell" ref={shellRef}>
|
|
||||||
<DropOverlay
|
|
||||||
active={dropOverlayState.active}
|
|
||||||
folderName={dropOverlayState.folderName}
|
|
||||||
/>
|
|
||||||
<UploadQueueOverlay
|
|
||||||
queue={contextValue.uploadQueue || []}
|
|
||||||
onClearQueue={contextValue.clearUploadQueue}
|
|
||||||
/>
|
|
||||||
<Outlet />
|
|
||||||
{managementModals}
|
|
||||||
{settingsOpen ? (
|
|
||||||
<SettingsRoute open onClose={closeSettings} />
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</AppShellContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppLayout;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
|
||||||
import AppLayout from './AppLayout';
|
|
||||||
import DocumentsRoute from './DocumentsRoute';
|
|
||||||
import LoginRoute from './LoginRoute';
|
|
||||||
|
|
||||||
const AppRouter = () => (
|
|
||||||
<Routes>
|
|
||||||
<Route path="/account/login" element={<LoginRoute />} />
|
|
||||||
<Route element={<AppLayout />}>
|
|
||||||
<Route path="/" element={<Navigate to="/documents" replace />} />
|
|
||||||
<Route path="/documents" element={<DocumentsRoute />} />
|
|
||||||
<Route path="/documents/folder/:folderId" element={<DocumentsRoute />} />
|
|
||||||
<Route path="/documents/:documentId" element={<DocumentsRoute />} />
|
|
||||||
<Route path="*" element={<Navigate to="/documents" replace />} />
|
|
||||||
</Route>
|
|
||||||
</Routes>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default AppRouter;
|
|
||||||
@@ -9,7 +9,7 @@ import React, {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { resolveDocumentAssetUrl } from '../asset_manager';
|
import { resolveDocumentAssetUrl } from '../asset_manager';
|
||||||
import type { EnsureAssetUrl, GetAsset } from '../asset_manager';
|
import type { EnsureAssetUrl, GetAsset } from '../asset_manager';
|
||||||
import { formatTransform } from './math';
|
import { formatTransform } from '../utils/math';
|
||||||
import useDocumentDrag from './useDocumentDrag';
|
import useDocumentDrag from './useDocumentDrag';
|
||||||
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
|
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
|
||||||
import {
|
import {
|
||||||
@@ -128,6 +128,8 @@ interface DesktopWorkspaceProps {
|
|||||||
activeTagFilters?: Array<Identifier | null>;
|
activeTagFilters?: Array<Identifier | null>;
|
||||||
tenantId?: Identifier | null;
|
tenantId?: Identifier | null;
|
||||||
viewId?: string | null;
|
viewId?: string | null;
|
||||||
|
documentLinks?: Map<Identifier, unknown> | null;
|
||||||
|
ensureDownloadUrl?: (docId: Identifier, options?: { force?: boolean }) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DesktopWorkspaceViewProps {
|
interface DesktopWorkspaceViewProps {
|
||||||
@@ -817,6 +819,8 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
overlayOriginTransform,
|
overlayOriginTransform,
|
||||||
overlayDocument,
|
overlayDocument,
|
||||||
onDocumentClick,
|
onDocumentClick,
|
||||||
|
handleStackSelect,
|
||||||
|
handlePromoteSelection,
|
||||||
onDocumentStackSelect: handleStackSelect,
|
onDocumentStackSelect: handleStackSelect,
|
||||||
onPromoteSelection: handlePromoteSelection,
|
onPromoteSelection: handlePromoteSelection,
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
@@ -862,6 +866,8 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
layoutRef,
|
layoutRef,
|
||||||
layoutSnapshot,
|
layoutSnapshot,
|
||||||
onDocumentClick,
|
onDocumentClick,
|
||||||
|
handleStackSelect,
|
||||||
|
handlePromoteSelection,
|
||||||
openOverlayForDoc,
|
openOverlayForDoc,
|
||||||
overlayDisplay,
|
overlayDisplay,
|
||||||
overlayOriginRect,
|
overlayOriginRect,
|
||||||
@@ -874,6 +880,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
resolveBaseMetrics,
|
resolveBaseMetrics,
|
||||||
setDraggingId,
|
setDraggingId,
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
|
clearSelection,
|
||||||
onDocumentActivate,
|
onDocumentActivate,
|
||||||
markLayoutDirty,
|
markLayoutDirty,
|
||||||
tagDropTargetId,
|
tagDropTargetId,
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
export { clamp } from '../utils/math';
|
|
||||||
|
|
||||||
export const formatTransform = (
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
rotation = 0,
|
|
||||||
scale = 1,
|
|
||||||
): string => `translate3d(${x}px, ${y}px, 0) rotate(${rotation}deg) scale(${scale})`;
|
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import type { PointerEvent as ReactPointerEvent } from 'react';
|
import type { PointerEvent as ReactPointerEvent } from 'react';
|
||||||
import { preventAll, safeInvoke } from './events';
|
import { preventAll, safeInvoke } from './events';
|
||||||
import { clamp } from './math';
|
import { clamp } from '../utils/math';
|
||||||
import usePointerTap from '../ui/usePointerTap';
|
import usePointerTap from '../ui/usePointerTap';
|
||||||
import {
|
import {
|
||||||
MIN_TIMESTEP,
|
MIN_TIMESTEP,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { clamp, formatTransform } from './math';
|
import { clamp, formatTransform } from '../utils/math';
|
||||||
import { fetchLayoutRecords, upsertLayoutRecords } from './db';
|
import { fetchLayoutRecords, upsertLayoutRecords } from './db';
|
||||||
|
|
||||||
type DocumentId = string;
|
type DocumentId = string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import type { DragEvent } from 'react';
|
import type { DragEvent } from 'react';
|
||||||
import { isPlainObject, isFunctionValue } from '../../utils/typeGuards';
|
import { isPlainObject } from '../../utils/typeGuards';
|
||||||
|
|
||||||
type Identifier = string | number;
|
type Identifier = string | number;
|
||||||
type FolderIdentifier = Identifier | 'root';
|
type FolderIdentifier = Identifier | 'root';
|
||||||
@@ -150,7 +150,7 @@ const useDocumentDragHandlers = ({
|
|||||||
return (payload as { id?: FolderIdentifier }).id ?? null;
|
return (payload as { id?: FolderIdentifier }).id ?? null;
|
||||||
}
|
}
|
||||||
const maybeTrim = (payload as { trim?: () => string })?.trim;
|
const maybeTrim = (payload as { trim?: () => string })?.trim;
|
||||||
if (isFunctionValue(maybeTrim)) {
|
if (typeof maybeTrim === 'function') {
|
||||||
const nextValue = maybeTrim.call(payload);
|
const nextValue = maybeTrim.call(payload);
|
||||||
return nextValue || null;
|
return nextValue || null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
import AssetManager, { getAssetFromVersion } from '../../asset_manager';
|
import AssetManager, { getAssetFromVersion } from '../../asset_manager';
|
||||||
import useApiError from '../useApiError';
|
import useApiError from '../useApiError';
|
||||||
import TagManager from '../../tag_manager';
|
import TagManager from '../../tag_manager';
|
||||||
import usePasskeys from '../../settings/usePasskeys';
|
|
||||||
import { useManagementModals } from '../../app/useManagementModals';
|
import { useManagementModals } from '../../app/useManagementModals';
|
||||||
import { useAppDispatch, useAppState } from '../../app/appState';
|
import { useAppDispatch, useAppState } from '../../app/appState';
|
||||||
import { fetchAsset } from '../../lib/apiClient';
|
import { fetchAsset } from '../../lib/apiClient';
|
||||||
@@ -31,7 +30,6 @@ import useDocumentPreview from '../../app/useDocumentPreview';
|
|||||||
import useSidebarProps from '../../sidebar/useSidebarProps';
|
import useSidebarProps from '../../sidebar/useSidebarProps';
|
||||||
import {
|
import {
|
||||||
ASSET_PRESIGN_TTL_MS,
|
ASSET_PRESIGN_TTL_MS,
|
||||||
DEFAULT_FOLDER_NAME,
|
|
||||||
DEFAULT_SORT_DIRECTION,
|
DEFAULT_SORT_DIRECTION,
|
||||||
DEFAULT_SORT_FIELD,
|
DEFAULT_SORT_FIELD,
|
||||||
createRootNode,
|
createRootNode,
|
||||||
@@ -46,19 +44,20 @@ import {
|
|||||||
import useDocumentsSearch from '../../app/useDocumentsSearch';
|
import useDocumentsSearch from '../../app/useDocumentsSearch';
|
||||||
import useDocumentsStore from './store/useDocumentsStore';
|
import useDocumentsStore from './store/useDocumentsStore';
|
||||||
import useAuthManager from './useAuthManager';
|
import useAuthManager from './useAuthManager';
|
||||||
import useTags from './useTags';
|
|
||||||
import useCorrespondents from './useCorrespondents';
|
|
||||||
import useTenantManager from './useTenantManager';
|
import useTenantManager from './useTenantManager';
|
||||||
import useDocuments from './useDocuments';
|
import useDocuments from './useDocuments';
|
||||||
import { fetchDocument } from '../../lib/apiClient';
|
import { fetchDocument } from '../../lib/apiClient';
|
||||||
import useFolderTree from './useFolderTree';
|
import useFolderTree from './useFolderTree';
|
||||||
import useFolderTreeActions from './useFolderTreeActions';
|
import useFolderTreeActions from './useFolderTreeActions';
|
||||||
import useDocumentTagging from './useDocumentTagging';
|
import useDocumentTagging from './useDocumentTagging';
|
||||||
import useDocumentCorrespondentActions from './useDocumentCorrespondentActions';
|
|
||||||
import useDocumentUploads from './useDocumentUploads';
|
import useDocumentUploads from './useDocumentUploads';
|
||||||
import useDocumentDragHandlers from './useDocumentDragHandlers';
|
import useDocumentDragHandlers from './useDocumentDragHandlers';
|
||||||
import useDocumentMutations from './useDocumentMutations';
|
import useDocumentMutations from './useDocumentMutations';
|
||||||
import useDetailWorkspace from '../../detail/useDetailWorkspace';
|
import useDetailWorkspace from '../../detail/useDetailWorkspace';
|
||||||
|
import useWorkspaceTaxonomies from './useWorkspaceTaxonomies';
|
||||||
|
import useWorkspaceBreadcrumbs from './useWorkspaceBreadcrumbs';
|
||||||
|
import useWorkspaceDeskProps from './useWorkspaceDeskProps';
|
||||||
|
import useWorkspaceSelectionSync from './useWorkspaceSelectionSync';
|
||||||
|
|
||||||
const EntryType = Object.freeze({
|
const EntryType = Object.freeze({
|
||||||
document: 'document',
|
document: 'document',
|
||||||
@@ -479,17 +478,7 @@ const useDocumentsWorkspace = ({
|
|||||||
const bootstrapInitializedRef = useRef(false);
|
const bootstrapInitializedRef = useRef(false);
|
||||||
const detailFolderFetchRef = useRef(new Set());
|
const detailFolderFetchRef = useRef(new Set());
|
||||||
|
|
||||||
|
useWorkspaceSelectionSync({
|
||||||
useEffect(() => {
|
|
||||||
if (!showingSearchResults) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSelectedEntries([]);
|
|
||||||
setSelectionOrder([]);
|
|
||||||
selectionOrderRef.current = [];
|
|
||||||
selectionAnchorRef.current = null;
|
|
||||||
setFocusedDocumentId(null);
|
|
||||||
}, [
|
|
||||||
showingSearchResults,
|
showingSearchResults,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
setSelectedEntries,
|
setSelectedEntries,
|
||||||
@@ -497,7 +486,11 @@ const useDocumentsWorkspace = ({
|
|||||||
selectionOrderRef,
|
selectionOrderRef,
|
||||||
selectionAnchorRef,
|
selectionAnchorRef,
|
||||||
setFocusedDocumentId,
|
setFocusedDocumentId,
|
||||||
]);
|
selectedDocumentIds,
|
||||||
|
activePreviewId,
|
||||||
|
setActivePreviewId,
|
||||||
|
selectionInitializedRef,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
tags,
|
tags,
|
||||||
@@ -506,59 +499,17 @@ const useDocumentsWorkspace = ({
|
|||||||
handleTagUpdate,
|
handleTagUpdate,
|
||||||
handleTagDelete,
|
handleTagDelete,
|
||||||
setTags,
|
setTags,
|
||||||
} = useTags({
|
tagLookupById,
|
||||||
apiClient,
|
|
||||||
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,
|
correspondents,
|
||||||
refreshCorrespondents,
|
refreshCorrespondents,
|
||||||
handleCorrespondentCreate,
|
handleCorrespondentCreate,
|
||||||
handleCorrespondentUpdate,
|
handleCorrespondentUpdate,
|
||||||
handleCorrespondentDelete,
|
handleCorrespondentDelete,
|
||||||
setCorrespondents,
|
setCorrespondents,
|
||||||
} = useCorrespondents({
|
correspondentLookupByName,
|
||||||
apiClient,
|
handleDocumentCorrespondentAttach,
|
||||||
notifyApiError,
|
handleCorrespondentRemove,
|
||||||
setStatusMessage,
|
handleCorrespondentAdd,
|
||||||
tenantIdRef,
|
|
||||||
mapDocumentCaches,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
passkeys,
|
passkeys,
|
||||||
passkeysSupported,
|
passkeysSupported,
|
||||||
passkeysLoading,
|
passkeysLoading,
|
||||||
@@ -567,9 +518,16 @@ const useDocumentsWorkspace = ({
|
|||||||
refreshPasskeys,
|
refreshPasskeys,
|
||||||
registerPasskey,
|
registerPasskey,
|
||||||
revokePasskey,
|
revokePasskey,
|
||||||
} = usePasskeys({
|
} = useWorkspaceTaxonomies({
|
||||||
|
apiClient,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
setStatusMessage,
|
setStatusMessage,
|
||||||
|
tagManager,
|
||||||
|
tenantIdRef,
|
||||||
|
currentTenantId,
|
||||||
|
setActiveTagFilters,
|
||||||
|
mapDocumentCaches,
|
||||||
|
updateDocumentCaches,
|
||||||
token,
|
token,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -647,20 +605,6 @@ const useDocumentsWorkspace = ({
|
|||||||
documentsViewMode,
|
documentsViewMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
|
||||||
correspondentLookupByName,
|
|
||||||
handleDocumentCorrespondentAttach,
|
|
||||||
handleCorrespondentRemove,
|
|
||||||
handleCorrespondentAdd,
|
|
||||||
} = useDocumentCorrespondentActions({
|
|
||||||
apiClient,
|
|
||||||
correspondents,
|
|
||||||
handleCorrespondentCreate,
|
|
||||||
notifyApiError,
|
|
||||||
setStatusMessage,
|
|
||||||
updateDocumentCaches,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeSortRefreshReadyRef.current) {
|
if (!activeSortRefreshReadyRef.current) {
|
||||||
activeSortRefreshReadyRef.current = true;
|
activeSortRefreshReadyRef.current = true;
|
||||||
@@ -1270,85 +1214,13 @@ const useDocumentsWorkspace = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { breadcrumbs, missingBreadcrumbAncestors } = useMemo(() => {
|
const breadcrumbs = useWorkspaceBreadcrumbs({
|
||||||
const chain = [];
|
selectedFolder,
|
||||||
const seen = new Set();
|
folderNodes,
|
||||||
const pending = new Set();
|
currentFolder,
|
||||||
let currentId = selectedFolder || 'root';
|
breadcrumbFetchRef,
|
||||||
let guard = 0;
|
ensureFolderData,
|
||||||
|
});
|
||||||
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({
|
const { handleTenantSelect } = useTenantManager({
|
||||||
apiClient,
|
apiClient,
|
||||||
@@ -1367,89 +1239,27 @@ const useDocumentsWorkspace = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const handleDeskDocumentStackSelect = useCallback(
|
const deskWorkspaceProps = useWorkspaceDeskProps({
|
||||||
(docIds: Array<Identifier | string>) => {
|
viewDocuments,
|
||||||
if (!Array.isArray(docIds) || docIds.length === 0) {
|
inspectDocumentForDesk,
|
||||||
return;
|
handleEntryPointer: handleEntryPointerCore,
|
||||||
}
|
selectedEntries,
|
||||||
|
selectionAnchorRef,
|
||||||
const rowKeys = docIds
|
applySelection,
|
||||||
.map((id) => resolveDocumentRowKey(id as Identifier))
|
resolveDocumentRowKey,
|
||||||
.filter((value): value is string => typeof value === 'string');
|
|
||||||
|
|
||||||
if (!rowKeys.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextKeys = [...selectedEntries];
|
|
||||||
rowKeys.forEach((key) => {
|
|
||||||
if (!nextKeys.includes(key)) {
|
|
||||||
nextKeys.push(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const anchor = (rowKeys[0]
|
|
||||||
|| selectionAnchorRef.current
|
|
||||||
|| nextKeys[nextKeys.length - 1]) as Identifier | string | null;
|
|
||||||
|
|
||||||
applySelection(nextKeys, {
|
|
||||||
anchor,
|
|
||||||
interactedKeys: rowKeys,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[applySelection, selectedEntries, selectionAnchorRef],
|
|
||||||
);
|
|
||||||
|
|
||||||
const deskViewId = useMemo(() => {
|
|
||||||
if (showingSearchResults) {
|
|
||||||
const trimmedQuery = searchQuery.trim();
|
|
||||||
const tagsKey = [...activeTagFilters].sort().join(',');
|
|
||||||
const correspondentsKey = [...activeCorrespondentFilters].sort().join(',');
|
|
||||||
return `search:${trimmedQuery}|tags:${tagsKey}|corr:${correspondentsKey}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const folderKey = selectedFolder && selectedFolder !== '' ? selectedFolder : 'root';
|
|
||||||
return `folder:${folderKey}`;
|
|
||||||
}, [
|
|
||||||
showingSearchResults,
|
showingSearchResults,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
activeTagFilters,
|
activeTagFilters,
|
||||||
activeCorrespondentFilters,
|
activeCorrespondentFilters,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
]);
|
promoteSelectionOrder,
|
||||||
|
handleDocumentTagDrop,
|
||||||
const deskWorkspaceProps = useMemo(
|
ensureAssetUrl,
|
||||||
() => ({
|
getDocumentAsset,
|
||||||
entries: viewDocuments,
|
currentTenantId,
|
||||||
onDocumentActivate: inspectDocumentForDesk,
|
documentLinks,
|
||||||
onDocumentClick: handleEntryPointerCore,
|
ensureDownloadUrl,
|
||||||
onDocumentStackSelect: handleDeskDocumentStackSelect,
|
});
|
||||||
onPromoteSelection: promoteSelectionOrder,
|
|
||||||
onDocumentTagDrop: handleDocumentTagDrop,
|
|
||||||
ensureAssetUrl,
|
|
||||||
getDocumentAsset,
|
|
||||||
activeTagFilters,
|
|
||||||
tenantId: currentTenantId,
|
|
||||||
viewId: deskViewId,
|
|
||||||
documentLinks,
|
|
||||||
ensureDownloadUrl,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
viewDocuments,
|
|
||||||
inspectDocumentForDesk,
|
|
||||||
handleEntryPointerCore,
|
|
||||||
handleDeskDocumentStackSelect,
|
|
||||||
promoteSelectionOrder,
|
|
||||||
handleDocumentTagDrop,
|
|
||||||
ensureAssetUrl,
|
|
||||||
getDocumentAsset,
|
|
||||||
activeTagFilters,
|
|
||||||
currentTenantId,
|
|
||||||
deskViewId,
|
|
||||||
documentLinks,
|
|
||||||
ensureDownloadUrl,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const documentsPanelProps = useDocumentsPanelProps({
|
const documentsPanelProps = useDocumentsPanelProps({
|
||||||
currentFolderName,
|
currentFolderName,
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import React, { useEffect, useMemo } from 'react';
|
||||||
|
import { DEFAULT_FOLDER_NAME } from '../../app/appLayoutUtils';
|
||||||
|
|
||||||
|
type Identifier = string | number;
|
||||||
|
type FolderId = Identifier | 'root';
|
||||||
|
|
||||||
|
interface UseWorkspaceBreadcrumbsArgs {
|
||||||
|
selectedFolder: FolderId | null;
|
||||||
|
folderNodes: Map<FolderId, { id?: FolderId; name?: string | null; parentId?: FolderId | null; parent_id?: FolderId | null }>;
|
||||||
|
currentFolder: { id?: FolderId; name?: string | null; parentId?: FolderId | null; parent_id?: FolderId | null } | null;
|
||||||
|
breadcrumbFetchRef: React.MutableRefObject<Set<FolderId>>;
|
||||||
|
ensureFolderData: (folderId: FolderId, options?: Record<string, unknown>) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useWorkspaceBreadcrumbs = ({
|
||||||
|
selectedFolder,
|
||||||
|
folderNodes,
|
||||||
|
currentFolder,
|
||||||
|
breadcrumbFetchRef,
|
||||||
|
ensureFolderData,
|
||||||
|
}: UseWorkspaceBreadcrumbsArgs) => {
|
||||||
|
const { breadcrumbs, missingBreadcrumbAncestors } = useMemo(() => {
|
||||||
|
const chain: Array<{ id: FolderId; name?: string | null }> = [];
|
||||||
|
const seen = new Set<FolderId>();
|
||||||
|
const pending = new Set<FolderId>();
|
||||||
|
let currentId: FolderId | null = (selectedFolder || 'root') as FolderId;
|
||||||
|
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 as FolderId);
|
||||||
|
if (node) {
|
||||||
|
chain.push({ id: currentId, name: node.name || 'Folder' });
|
||||||
|
currentId = (node.parentId ?? node.parent_id ?? 'root') as FolderId;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fallbackName: string | null | undefined = '…';
|
||||||
|
let parentId: FolderId | null | undefined = null;
|
||||||
|
|
||||||
|
if (currentFolder && currentFolder.id === currentId) {
|
||||||
|
fallbackName = currentFolder.name;
|
||||||
|
parentId = (currentFolder.parent_id ?? currentFolder.parentId ?? 'root') as FolderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.push({ id: currentId, name: fallbackName });
|
||||||
|
pending.add(currentId);
|
||||||
|
currentId = parentId as FolderId | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chain.some((crumb) => crumb.id === 'root')) {
|
||||||
|
chain.push({ id: 'root', name: DEFAULT_FOLDER_NAME });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ordered: Array<{ id: FolderId; name?: string | null }> = [];
|
||||||
|
const seenOrdered = new Set<FolderId>();
|
||||||
|
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, breadcrumbFetchRef]);
|
||||||
|
|
||||||
|
return breadcrumbs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useWorkspaceBreadcrumbs;
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import type { MutableRefObject } from 'react';
|
||||||
|
|
||||||
|
type Identifier = string | number;
|
||||||
|
|
||||||
|
interface UseWorkspaceDeskPropsArgs {
|
||||||
|
viewDocuments: any[];
|
||||||
|
inspectDocumentForDesk: (doc: any) => void;
|
||||||
|
handleEntryPointer: (params: { rowKey?: string | null; id?: Identifier | null; type?: string; event?: any }) => void;
|
||||||
|
selectedEntries: Array<string | number>;
|
||||||
|
selectionAnchorRef: MutableRefObject<Identifier | string | null>;
|
||||||
|
applySelection: (rowKeys: Array<string | number>, options?: { anchor?: Identifier | string | null; interactedKeys?: Array<string | number> }) => void;
|
||||||
|
resolveDocumentRowKey: (id?: Identifier | null) => string | null;
|
||||||
|
showingSearchResults: boolean;
|
||||||
|
searchQuery: string;
|
||||||
|
activeTagFilters: Array<string | number>;
|
||||||
|
activeCorrespondentFilters: Array<string | number>;
|
||||||
|
selectedFolder: Identifier | 'root' | null;
|
||||||
|
promoteSelectionOrder: () => void;
|
||||||
|
handleDocumentTagDrop: (docId: Identifier, tagId: Identifier) => Promise<void> | void;
|
||||||
|
ensureAssetUrl: (docId: Identifier, asset: any, options?: Record<string, unknown>) => Promise<any> | null;
|
||||||
|
getDocumentAsset: (doc: any, type: string) => any;
|
||||||
|
currentTenantId: Identifier | null;
|
||||||
|
documentLinks: Map<Identifier, unknown> | null;
|
||||||
|
ensureDownloadUrl?: (docId: Identifier, options?: { force?: boolean }) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useWorkspaceDeskProps = ({
|
||||||
|
viewDocuments,
|
||||||
|
inspectDocumentForDesk,
|
||||||
|
handleEntryPointer,
|
||||||
|
selectedEntries,
|
||||||
|
selectionAnchorRef,
|
||||||
|
applySelection,
|
||||||
|
resolveDocumentRowKey,
|
||||||
|
showingSearchResults,
|
||||||
|
searchQuery,
|
||||||
|
activeTagFilters,
|
||||||
|
activeCorrespondentFilters,
|
||||||
|
selectedFolder,
|
||||||
|
promoteSelectionOrder,
|
||||||
|
handleDocumentTagDrop,
|
||||||
|
ensureAssetUrl,
|
||||||
|
getDocumentAsset,
|
||||||
|
currentTenantId,
|
||||||
|
documentLinks,
|
||||||
|
ensureDownloadUrl,
|
||||||
|
}: UseWorkspaceDeskPropsArgs) => {
|
||||||
|
const handleDeskDocumentStackSelect = useCallback(
|
||||||
|
(docIds: Array<Identifier | string>) => {
|
||||||
|
if (!Array.isArray(docIds) || docIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowKeys = docIds
|
||||||
|
.map((id) => resolveDocumentRowKey(id as Identifier))
|
||||||
|
.filter((value): value is string => typeof value === 'string');
|
||||||
|
|
||||||
|
if (!rowKeys.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextKeys = [...selectedEntries];
|
||||||
|
rowKeys.forEach((key) => {
|
||||||
|
if (!nextKeys.includes(key)) {
|
||||||
|
nextKeys.push(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const anchor = (rowKeys[0]
|
||||||
|
|| selectionAnchorRef.current
|
||||||
|
|| nextKeys[nextKeys.length - 1]) as Identifier | string | null;
|
||||||
|
|
||||||
|
applySelection(nextKeys, {
|
||||||
|
anchor,
|
||||||
|
interactedKeys: rowKeys,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[applySelection, resolveDocumentRowKey, selectedEntries, selectionAnchorRef],
|
||||||
|
);
|
||||||
|
|
||||||
|
const deskViewId = useMemo(() => {
|
||||||
|
if (showingSearchResults) {
|
||||||
|
const trimmedQuery = searchQuery.trim();
|
||||||
|
const tagsKey = [...activeTagFilters].sort().join(',');
|
||||||
|
const correspondentsKey = [...activeCorrespondentFilters].sort().join(',');
|
||||||
|
return `search:${trimmedQuery}|tags:${tagsKey}|corr:${correspondentsKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderKey = selectedFolder && selectedFolder !== '' ? selectedFolder : 'root';
|
||||||
|
return `folder:${folderKey}`;
|
||||||
|
}, [
|
||||||
|
showingSearchResults,
|
||||||
|
searchQuery,
|
||||||
|
activeTagFilters,
|
||||||
|
activeCorrespondentFilters,
|
||||||
|
selectedFolder,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const deskWorkspaceProps = useMemo(
|
||||||
|
() => ({
|
||||||
|
entries: viewDocuments,
|
||||||
|
onDocumentActivate: inspectDocumentForDesk,
|
||||||
|
onDocumentClick: handleEntryPointer,
|
||||||
|
onDocumentStackSelect: handleDeskDocumentStackSelect,
|
||||||
|
onPromoteSelection: promoteSelectionOrder,
|
||||||
|
onDocumentTagDrop: handleDocumentTagDrop,
|
||||||
|
ensureAssetUrl,
|
||||||
|
getDocumentAsset,
|
||||||
|
activeTagFilters,
|
||||||
|
tenantId: currentTenantId,
|
||||||
|
viewId: deskViewId,
|
||||||
|
documentLinks,
|
||||||
|
ensureDownloadUrl,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
viewDocuments,
|
||||||
|
inspectDocumentForDesk,
|
||||||
|
handleEntryPointer,
|
||||||
|
handleDeskDocumentStackSelect,
|
||||||
|
promoteSelectionOrder,
|
||||||
|
handleDocumentTagDrop,
|
||||||
|
ensureAssetUrl,
|
||||||
|
getDocumentAsset,
|
||||||
|
activeTagFilters,
|
||||||
|
currentTenantId,
|
||||||
|
deskViewId,
|
||||||
|
documentLinks,
|
||||||
|
ensureDownloadUrl,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return deskWorkspaceProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useWorkspaceDeskProps;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import type { MutableRefObject } from 'react';
|
||||||
|
|
||||||
|
type Identifier = string | number;
|
||||||
|
|
||||||
|
interface UseWorkspaceSelectionSyncArgs {
|
||||||
|
showingSearchResults: boolean;
|
||||||
|
searchQuery: string;
|
||||||
|
setSelectedEntries: (entries: Array<string | number>) => void;
|
||||||
|
setSelectionOrder: (order: Array<string | number>) => void;
|
||||||
|
selectionOrderRef: MutableRefObject<Array<string | number>>;
|
||||||
|
selectionAnchorRef: MutableRefObject<Identifier | string | null>;
|
||||||
|
setFocusedDocumentId: (id: Identifier | null) => void;
|
||||||
|
selectedDocumentIds: Identifier[];
|
||||||
|
activePreviewId: Identifier | null;
|
||||||
|
setActivePreviewId: (id: Identifier | null) => void;
|
||||||
|
selectionInitializedRef: MutableRefObject<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useWorkspaceSelectionSync = ({
|
||||||
|
showingSearchResults,
|
||||||
|
searchQuery,
|
||||||
|
setSelectedEntries,
|
||||||
|
setSelectionOrder,
|
||||||
|
selectionOrderRef,
|
||||||
|
selectionAnchorRef,
|
||||||
|
setFocusedDocumentId,
|
||||||
|
selectedDocumentIds,
|
||||||
|
activePreviewId,
|
||||||
|
setActivePreviewId,
|
||||||
|
selectionInitializedRef,
|
||||||
|
}: UseWorkspaceSelectionSyncArgs) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showingSearchResults) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedEntries([]);
|
||||||
|
setSelectionOrder([]);
|
||||||
|
selectionOrderRef.current = [];
|
||||||
|
selectionAnchorRef.current = null;
|
||||||
|
setFocusedDocumentId(null);
|
||||||
|
}, [
|
||||||
|
showingSearchResults,
|
||||||
|
searchQuery,
|
||||||
|
setSelectedEntries,
|
||||||
|
setSelectionOrder,
|
||||||
|
selectionOrderRef,
|
||||||
|
selectionAnchorRef,
|
||||||
|
setFocusedDocumentId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedDocumentIds.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!selectedDocumentIds.includes(activePreviewId as Identifier)) {
|
||||||
|
setActivePreviewId(selectedDocumentIds[selectedDocumentIds.length - 1]);
|
||||||
|
}
|
||||||
|
selectionInitializedRef.current = true;
|
||||||
|
}, [selectedDocumentIds, activePreviewId, selectionInitializedRef, setActivePreviewId]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useWorkspaceSelectionSync;
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||||
|
import usePasskeys from '../../settings/usePasskeys';
|
||||||
|
import TagManager from '../../tag_manager';
|
||||||
|
import useCorrespondents from './useCorrespondents';
|
||||||
|
import useDocumentCorrespondentActions from './useDocumentCorrespondentActions';
|
||||||
|
import useTags from './useTags';
|
||||||
|
|
||||||
|
type Identifier = string | number;
|
||||||
|
|
||||||
|
interface UseWorkspaceTaxonomiesArgs {
|
||||||
|
apiClient: any;
|
||||||
|
notifyApiError: (error: unknown, fallbackMessage?: string, variant?: string) => void;
|
||||||
|
setStatusMessage: (message: string, variant?: string) => void;
|
||||||
|
tagManager: TagManager;
|
||||||
|
tenantIdRef: MutableRefObject<Identifier | null>;
|
||||||
|
currentTenantId: Identifier | null;
|
||||||
|
setActiveTagFilters: Dispatch<SetStateAction<Identifier[]>>;
|
||||||
|
mapDocumentCaches: (mapper: (doc: any) => any | undefined) => void;
|
||||||
|
updateDocumentCaches: (id: Identifier, updater: (doc: any) => any) => void;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useWorkspaceTaxonomies = ({
|
||||||
|
apiClient,
|
||||||
|
notifyApiError,
|
||||||
|
setStatusMessage,
|
||||||
|
tagManager,
|
||||||
|
tenantIdRef,
|
||||||
|
currentTenantId,
|
||||||
|
setActiveTagFilters,
|
||||||
|
mapDocumentCaches,
|
||||||
|
updateDocumentCaches,
|
||||||
|
token,
|
||||||
|
}: UseWorkspaceTaxonomiesArgs) => {
|
||||||
|
const {
|
||||||
|
tags,
|
||||||
|
refreshTags,
|
||||||
|
handleTagCreate,
|
||||||
|
handleTagUpdate,
|
||||||
|
handleTagDelete,
|
||||||
|
setTags,
|
||||||
|
} = useTags({
|
||||||
|
apiClient,
|
||||||
|
notifyApiError,
|
||||||
|
setStatusMessage,
|
||||||
|
tagManager,
|
||||||
|
tenantIdRef,
|
||||||
|
setActiveTagFilters,
|
||||||
|
mapDocumentCaches,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
tenantIdRef.current = currentTenantId;
|
||||||
|
}, [currentTenantId, tenantIdRef]);
|
||||||
|
|
||||||
|
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,
|
||||||
|
notifyApiError,
|
||||||
|
setStatusMessage,
|
||||||
|
tenantIdRef,
|
||||||
|
mapDocumentCaches,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
correspondentLookupByName,
|
||||||
|
handleDocumentCorrespondentAttach,
|
||||||
|
handleCorrespondentRemove,
|
||||||
|
handleCorrespondentAdd,
|
||||||
|
} = useDocumentCorrespondentActions({
|
||||||
|
apiClient,
|
||||||
|
correspondents,
|
||||||
|
handleCorrespondentCreate,
|
||||||
|
notifyApiError,
|
||||||
|
setStatusMessage,
|
||||||
|
updateDocumentCaches,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
passkeys,
|
||||||
|
passkeysSupported,
|
||||||
|
passkeysLoading,
|
||||||
|
registeringPasskey,
|
||||||
|
revokingPasskeyId,
|
||||||
|
refreshPasskeys,
|
||||||
|
registerPasskey,
|
||||||
|
revokePasskey,
|
||||||
|
} = usePasskeys({
|
||||||
|
notifyApiError,
|
||||||
|
setStatusMessage,
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tags,
|
||||||
|
refreshTags,
|
||||||
|
handleTagCreate,
|
||||||
|
handleTagUpdate,
|
||||||
|
handleTagDelete,
|
||||||
|
setTags,
|
||||||
|
tagLookupById,
|
||||||
|
correspondents,
|
||||||
|
refreshCorrespondents,
|
||||||
|
handleCorrespondentCreate,
|
||||||
|
handleCorrespondentUpdate,
|
||||||
|
handleCorrespondentDelete,
|
||||||
|
setCorrespondents,
|
||||||
|
correspondentLookupByName,
|
||||||
|
handleDocumentCorrespondentAttach,
|
||||||
|
handleCorrespondentRemove,
|
||||||
|
handleCorrespondentAdd,
|
||||||
|
passkeys,
|
||||||
|
passkeysSupported,
|
||||||
|
passkeysLoading,
|
||||||
|
registeringPasskey,
|
||||||
|
revokingPasskeyId,
|
||||||
|
refreshPasskeys,
|
||||||
|
registerPasskey,
|
||||||
|
revokePasskey,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useWorkspaceTaxonomies;
|
||||||
+85
-2
@@ -2,10 +2,93 @@ import '@fontsource/inter/400.css';
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { HashRouter } from 'react-router-dom';
|
import {
|
||||||
|
HashRouter,
|
||||||
|
Navigate,
|
||||||
|
Outlet,
|
||||||
|
Route,
|
||||||
|
Routes,
|
||||||
|
} from 'react-router-dom';
|
||||||
import './styles/index.css';
|
import './styles/index.css';
|
||||||
|
import DocumentsRoute from './app/DocumentsRoute';
|
||||||
|
import DropOverlay from './app/DropOverlay';
|
||||||
|
import LoginRoute from './app/LoginRoute';
|
||||||
|
import SettingsRoute from './app/SettingsRoute';
|
||||||
import { AppStateProvider } from './app/appState';
|
import { AppStateProvider } from './app/appState';
|
||||||
import AppRouter from './app/AppRouter';
|
import { useDocumentsPreferences } from './app/useDocumentsPreferences';
|
||||||
|
import { AppShellContext } from './appShellContext';
|
||||||
|
import useDocumentsWorkspace from './hooks/documents/useDocumentsWorkspace';
|
||||||
|
import UploadQueueOverlay from './app/UploadQueueOverlay';
|
||||||
|
|
||||||
|
const AppLayout: React.FC = () => {
|
||||||
|
const documentsPreferences = useDocumentsPreferences();
|
||||||
|
const {
|
||||||
|
appStatus,
|
||||||
|
location,
|
||||||
|
shellRef,
|
||||||
|
dropOverlayState,
|
||||||
|
managementModals,
|
||||||
|
contextValue,
|
||||||
|
settingsOpen,
|
||||||
|
closeSettings,
|
||||||
|
} = useDocumentsWorkspace({
|
||||||
|
documentsViewMode: documentsPreferences.documentsViewMode,
|
||||||
|
documentsSortField: documentsPreferences.documentsSortField,
|
||||||
|
documentsSortDirection: documentsPreferences.documentsSortDirection,
|
||||||
|
documentsSortFieldRef: documentsPreferences.documentsSortFieldRef,
|
||||||
|
documentsSortDirectionRef: documentsPreferences.documentsSortDirectionRef,
|
||||||
|
onDocumentsViewModeChange: documentsPreferences.handleDocumentsViewModeChange,
|
||||||
|
onDocumentsSortFieldChange: documentsPreferences.handleDocumentsSortFieldChange,
|
||||||
|
onDocumentsSortDirectionToggle: documentsPreferences.handleDocumentsSortDirectionToggle,
|
||||||
|
searchIncludeDescendants: documentsPreferences.searchIncludeDescendants,
|
||||||
|
onSetSearchIncludeDescendants: documentsPreferences.setSearchIncludeDescendants,
|
||||||
|
sortRefreshReadyRef: documentsPreferences.sortRefreshReadyRef,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (['logged-out', 'authenticating', 'selecting-tenant'].includes(appStatus)) {
|
||||||
|
const redirectTarget = `${location.pathname}${location.search}${location.hash || ''}`;
|
||||||
|
return (
|
||||||
|
<Navigate
|
||||||
|
to="/account/login"
|
||||||
|
replace
|
||||||
|
state={{ from: redirectTarget }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShellContext.Provider value={contextValue}>
|
||||||
|
<div className="app-shell" ref={shellRef}>
|
||||||
|
<DropOverlay
|
||||||
|
active={dropOverlayState.active}
|
||||||
|
folderName={dropOverlayState.folderName}
|
||||||
|
/>
|
||||||
|
<UploadQueueOverlay
|
||||||
|
queue={contextValue.uploadQueue || []}
|
||||||
|
onClearQueue={contextValue.clearUploadQueue}
|
||||||
|
/>
|
||||||
|
<Outlet />
|
||||||
|
{managementModals}
|
||||||
|
{settingsOpen ? (
|
||||||
|
<SettingsRoute open onClose={closeSettings} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</AppShellContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppRouter: React.FC = () => (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/account/login" element={<LoginRoute />} />
|
||||||
|
<Route element={<AppLayout />}>
|
||||||
|
<Route path="/" element={<Navigate to="/documents" replace />} />
|
||||||
|
<Route path="/documents" element={<DocumentsRoute />} />
|
||||||
|
<Route path="/documents/folder/:folderId" element={<DocumentsRoute />} />
|
||||||
|
<Route path="/documents/:documentId" element={<DocumentsRoute />} />
|
||||||
|
<Route path="*" element={<Navigate to="/documents" replace />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
|
||||||
const container = document.getElementById('app');
|
const container = document.getElementById('app');
|
||||||
|
|
||||||
|
|||||||
Vendored
-4
@@ -1,4 +0,0 @@
|
|||||||
declare module '*?url' {
|
|
||||||
const url: string;
|
|
||||||
export default url;
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,14 @@ export const clamp = (value: number, min: number, max: number): number => {
|
|||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatTransform = (
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
rotation = 0,
|
||||||
|
scale = 1,
|
||||||
|
): string => `translate3d(${x}px, ${y}px, 0) rotate(${rotation}deg) scale(${scale})`;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
clamp,
|
clamp,
|
||||||
|
formatTransform,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,3 @@ export const isPlainObject = (value: unknown): value is Record<string, unknown>
|
|||||||
|
|
||||||
export const isStringValue = (value: unknown): value is string =>
|
export const isStringValue = (value: unknown): value is string =>
|
||||||
objectToString.call(value) === '[object String]';
|
objectToString.call(value) === '[object String]';
|
||||||
|
|
||||||
export const isFunctionValue = <T extends (...args: unknown[]) => unknown>(value: unknown): value is T =>
|
|
||||||
objectToString.call(value) === '[object Function]';
|
|
||||||
|
|||||||
Reference in New Issue
Block a user