diff --git a/frontend/src/app/AppLayout.jsx b/frontend/src/app/AppLayout.jsx
index bc355e4..c1496d1 100644
--- a/frontend/src/app/AppLayout.jsx
+++ b/frontend/src/app/AppLayout.jsx
@@ -13,14 +13,11 @@ import AssetManager, {
createAssetView,
} from '../asset_manager';
import useApiError from '../hooks/useApiError';
-import TagsPanel from '../tags/TagsPanel';
-import CorrespondentsPanel from '../correspondents/CorrespondentsPanel';
-import DocumentTypesPanel from '../documentTypes/DocumentTypesPanel';
import TagManager from '../tag_manager';
-import SettingsModal from '../settings/SettingsModal';
import usePasskeys from '../settings/usePasskeys';
import { AppShellContext } from '../appShellContext';
import DropOverlay from './DropOverlay';
+import { useManagementModals } from './useManagementModals';
import { api, useAppDispatch, useAppState } from './appState';
const ASSET_PRESIGN_TTL_MS = 240 * 1000; // backend issues 5 min tokens; refresh slightly early
@@ -182,10 +179,6 @@ const AppLayout = () => {
[reportApiError],
);
const [loading, setLoading] = useState(false);
- const [isTagsModalOpen, setTagsModalOpen] = useState(false);
- const [isCorrespondentsModalOpen, setCorrespondentsModalOpen] = useState(false);
- const [isDocumentTypesModalOpen, setDocumentTypesModalOpen] = useState(false);
- const [isSettingsModalOpen, setSettingsModalOpen] = useState(false);
const [creatingFolder, setCreatingFolder] = useState(false);
const [folderNodes, setFolderNodes] = useState(() => {
const rootNode = createRootNode();
@@ -1596,20 +1589,6 @@ const AppLayout = () => {
token,
});
- useEffect(() => {
- if (!isSettingsModalOpen) {
- return;
- }
- refreshWebdavTokens();
- refreshPasskeys();
- }, [isSettingsModalOpen, refreshPasskeys, refreshWebdavTokens]);
-
- useEffect(() => {
- if (!isSettingsModalOpen) {
- setWebdavTokenSecret(null);
- }
- }, [isSettingsModalOpen]);
-
const handleTagUpdate = useCallback(
async (tagId, changes) => {
if (!tagId) {
@@ -4059,83 +4038,34 @@ const AppLayout = () => {
}
}, [creatingFolder, handleFolderCreate, setStatusMessage]);
- const openTagsModal = useCallback(() => {
- setCorrespondentsModalOpen(false);
- setDocumentTypesModalOpen(false);
- setTagsModalOpen(true);
- }, []);
+ const {
+ managementModals,
+ openTagsModal,
+ openCorrespondentsModal,
+ openDocumentTypesModal,
+ } = useManagementModals({
+ locationPathname: location.pathname,
+ tags,
+ refreshTags,
+ onTagCreate: handleTagCreate,
+ onTagUpdate: handleTagUpdate,
+ onTagDelete: handleTagDelete,
+ correspondents,
+ refreshCorrespondents,
+ onCorrespondentCreate: handleCorrespondentCreate,
+ onCorrespondentUpdate: handleCorrespondentUpdate,
+ onCorrespondentDelete: handleCorrespondentDelete,
+ documentTypes,
+ refreshDocumentTypes,
+ onDocumentTypeCreate: handleDocumentTypeCreate,
+ onDocumentTypeUpdate: handleDocumentTypeUpdate,
+ onDocumentTypeDelete: handleDocumentTypeDelete,
+ setStatusMessage,
+ });
- const closeTagsModal = useCallback(() => {
- setTagsModalOpen(false);
- }, []);
-
- const openCorrespondentsModal = useCallback(() => {
- setTagsModalOpen(false);
- setCorrespondentsModalOpen(true);
- setDocumentTypesModalOpen(false);
- }, []);
-
- const closeCorrespondentsModal = useCallback(() => {
- setCorrespondentsModalOpen(false);
- }, []);
-
- const openDocumentTypesModal = useCallback(() => {
- setTagsModalOpen(false);
- setCorrespondentsModalOpen(false);
- setDocumentTypesModalOpen(true);
- }, []);
-
- const closeDocumentTypesModal = useCallback(() => {
- setDocumentTypesModalOpen(false);
- }, []);
-
- const openSettingsModal = useCallback(() => {
- setSettingsModalOpen(true);
- }, []);
-
- const closeSettingsModal = useCallback(() => {
- setSettingsModalOpen(false);
- }, []);
-
- useEffect(() => {
- setTagsModalOpen(false);
- setCorrespondentsModalOpen(false);
- setDocumentTypesModalOpen(false);
- setSettingsModalOpen(false);
- }, [location.pathname]);
-
- useEffect(() => {
- if (!isTagsModalOpen && !isCorrespondentsModalOpen && !isDocumentTypesModalOpen && !isSettingsModalOpen) {
- return;
- }
- const handleKeyDown = (event) => {
- if (event.key === 'Escape') {
- event.preventDefault();
- if (isTagsModalOpen) {
- closeTagsModal();
- } else if (isCorrespondentsModalOpen) {
- closeCorrespondentsModal();
- } else if (isDocumentTypesModalOpen) {
- closeDocumentTypesModal();
- } else if (isSettingsModalOpen) {
- closeSettingsModal();
- }
- }
- };
- window.addEventListener('keydown', handleKeyDown);
- return () => {
- window.removeEventListener('keydown', handleKeyDown);
- };
- }, [
- isTagsModalOpen,
- isCorrespondentsModalOpen,
- isDocumentTypesModalOpen,
- isSettingsModalOpen,
- closeTagsModal,
- closeCorrespondentsModal,
- closeDocumentTypesModal,
- closeSettingsModal,
- ]);
+ const openSettings = useCallback(() => {
+ navigate('/settings');
+ }, [navigate]);
useEffect(() => {
if (!token) return undefined;
@@ -5216,7 +5146,7 @@ const AppLayout = () => {
tenants: tenantOptions,
activeTenantId: currentTenantId,
onSelectTenant: handleTenantSelect,
- onOpenSettings: openSettingsModal,
+ onOpenSettings: openSettings,
}),
[
activeCorrespondentFilters,
@@ -5237,7 +5167,7 @@ const AppLayout = () => {
handleSearchSubmit,
handleTenantSelect,
loading,
- openSettingsModal,
+ openSettings,
previewActive,
searchQuery,
draggedFolderId,
@@ -5401,6 +5331,23 @@ const AppLayout = () => {
handleDocumentTypeSet,
handleBulkDocumentTypeSet,
handleBulkDocumentTypeClear,
+ webdavTokens,
+ webdavTokensLoading,
+ creatingWebdavToken,
+ deletingWebdavTokenId,
+ refreshWebdavTokens,
+ createWebdavToken,
+ deleteWebdavToken,
+ webdavTokenSecret,
+ dismissCreatedWebdavToken,
+ passkeys,
+ passkeysSupported,
+ passkeysLoading,
+ registeringPasskey,
+ revokingPasskeyId,
+ refreshPasskeys,
+ registerPasskey,
+ revokePasskey,
previewActive,
previewWorkspaceDocument,
previewWorkspaceEntry,
@@ -5420,7 +5367,7 @@ const AppLayout = () => {
openTagsModal,
openCorrespondentsModal,
openDocumentTypesModal,
- openSettingsModal,
+ openSettings,
detailPanelOpen,
setDetailPanelOpen,
}),
@@ -5455,6 +5402,23 @@ const AppLayout = () => {
handleDocumentTypeSet,
handleBulkDocumentTypeSet,
handleBulkDocumentTypeClear,
+ webdavTokens,
+ webdavTokensLoading,
+ creatingWebdavToken,
+ deletingWebdavTokenId,
+ refreshWebdavTokens,
+ createWebdavToken,
+ deleteWebdavToken,
+ webdavTokenSecret,
+ dismissCreatedWebdavToken,
+ passkeys,
+ passkeysSupported,
+ passkeysLoading,
+ registeringPasskey,
+ revokingPasskeyId,
+ refreshPasskeys,
+ registerPasskey,
+ revokePasskey,
previewActive,
previewWorkspaceDocument,
previewWorkspaceEntry,
@@ -5473,7 +5437,7 @@ const AppLayout = () => {
openTagsModal,
openCorrespondentsModal,
openDocumentTypesModal,
- openSettingsModal,
+ openSettings,
detailPanelOpen,
setDetailPanelOpen,
],
@@ -5502,131 +5466,7 @@ const AppLayout = () => {
folderName={dropOverlayState.folderName}
/>
- {isTagsModalOpen && (
-
-
event.stopPropagation()}
- >
-
-
-
-
-
-
-
-
-
- )}
- {isCorrespondentsModalOpen && (
-
-
event.stopPropagation()}
- >
-
-
Manage Correspondents
-
-
-
-
-
-
-
-)}
- {isDocumentTypesModalOpen && (
-
-
event.stopPropagation()}
- >
-
-
Manage Document Types
-
-
-
-
-
-
-
- )}
-
+ {managementModals}
);
diff --git a/frontend/src/app/AppRouter.jsx b/frontend/src/app/AppRouter.jsx
index fb5488b..f077d05 100644
--- a/frontend/src/app/AppRouter.jsx
+++ b/frontend/src/app/AppRouter.jsx
@@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from 'react-router-dom';
import AppLayout from './AppLayout';
import DocumentsRoute from './DocumentsRoute';
import LoginRoute from './LoginRoute';
+import SettingsRoute from './SettingsRoute';
const AppRouter = () => (
@@ -12,6 +13,7 @@ const AppRouter = () => (
} />
} />
} />
+ } />
} />
diff --git a/frontend/src/app/DocumentsRoute.jsx b/frontend/src/app/DocumentsRoute.jsx
index 7fdef3a..2435085 100644
--- a/frontend/src/app/DocumentsRoute.jsx
+++ b/frontend/src/app/DocumentsRoute.jsx
@@ -1,11 +1,8 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
-import { ChevronsRightIcon } from '../ui/icons';
-import { createDocumentsSurface } from '../documents/DocumentsTable';
-import { createPreviewSurface } from '../preview/PreviewWorkspace';
-import { createDesktopSurface } from '../DesktopWorkspace';
import { useAppShell } from '../appShellContext';
import DocumentsLayout from './DocumentsLayout';
+import { useWorkspaceSurface } from './useWorkspaceSurface';
const DocumentsRoute = () => {
const {
@@ -63,66 +60,14 @@ const DocumentsRoute = () => {
navigate(target);
}, [navigate, parentBreadcrumb]);
- const isWorkspace = workspaceMode === 'skeuo';
- const isPreviewWorkspace = Boolean(previewWorkspaceDocument);
- const showPreviewWorkspace = !isWorkspace && isPreviewWorkspace;
-
- const renderSidebarToggle = useCallback(() => {
- if (!sidebarCollapsed) {
- return null;
- }
- return (
-
- );
- }, [sidebarCollapsed, expandSidebar]);
-
- const documentsSurface = useMemo(() => {
- if (!documentsTableProps) {
- return null;
- }
- return createDocumentsSurface({
- tableProps: documentsTableProps,
- parentBreadcrumb,
- onNavigateParent: parentBreadcrumb ? handleNavigateParent : null,
- renderSidebarToggle,
- detailProps: detailPanelProps,
- detailOpen: detailPanelOpen,
- });
- }, [
+ const { surface } = useWorkspaceSurface({
+ sidebarCollapsed,
+ onExpandSidebar: expandSidebar,
documentsTableProps,
- parentBreadcrumb,
- handleNavigateParent,
- renderSidebarToggle,
detailPanelProps,
detailPanelOpen,
- ]);
-
- const previewSurface = useMemo(() => {
- if (!showPreviewWorkspace || !previewWorkspaceDocument) {
- return null;
- }
- return createPreviewSurface({
- document: previewWorkspaceDocument,
- previewEntry: previewWorkspaceEntry,
- ensureAssetUrl,
- ensurePreviewData,
- getDocumentAsset,
- resolveApiPath,
- notifyApiError,
- onRegenerate: handleThumbnailRegeneration,
- onClose: closeDocumentPreview,
- renderSidebarToggle,
- });
- }, [
- showPreviewWorkspace,
+ workspaceMode,
+ skeuoWorkspaceProps,
previewWorkspaceDocument,
previewWorkspaceEntry,
ensureAssetUrl,
@@ -132,24 +77,9 @@ const DocumentsRoute = () => {
notifyApiError,
handleThumbnailRegeneration,
closeDocumentPreview,
- renderSidebarToggle,
- ]);
-
- const workspaceSurface = useMemo(() => {
- if (!isWorkspace) {
- return null;
- }
- return createDesktopSurface({
- workspaceProps: skeuoWorkspaceProps,
- renderSidebarToggle,
- });
- }, [isWorkspace, skeuoWorkspaceProps, renderSidebarToggle]);
-
- const surface = showPreviewWorkspace
- ? previewSurface
- : isWorkspace
- ? workspaceSurface
- : documentsSurface;
+ parentBreadcrumb,
+ onNavigateParent: handleNavigateParent,
+ });
if (!surface) {
return (
diff --git a/frontend/src/app/SettingsRoute.jsx b/frontend/src/app/SettingsRoute.jsx
new file mode 100644
index 0000000..dec6563
--- /dev/null
+++ b/frontend/src/app/SettingsRoute.jsx
@@ -0,0 +1,78 @@
+import React, { useEffect, useCallback } from 'react';
+import { useNavigate } from 'react-router-dom';
+import SettingsModal from '../settings/SettingsModal';
+import { useAppShell } from '../appShellContext';
+
+const SettingsRoute = () => {
+ const navigate = useNavigate();
+ const {
+ webdavTokens,
+ webdavTokensLoading,
+ creatingWebdavToken,
+ deletingWebdavTokenId,
+ refreshWebdavTokens,
+ createWebdavToken,
+ deleteWebdavToken,
+ webdavTokenSecret,
+ dismissCreatedWebdavToken,
+ passkeys,
+ passkeysSupported,
+ passkeysLoading,
+ registeringPasskey,
+ revokingPasskeyId,
+ refreshPasskeys,
+ registerPasskey,
+ revokePasskey,
+ } = useAppShell();
+
+ useEffect(() => {
+ refreshWebdavTokens();
+ refreshPasskeys();
+ }, [refreshWebdavTokens, refreshPasskeys]);
+
+ const handleClose = useCallback(() => {
+ dismissCreatedWebdavToken();
+ navigate(-1);
+ }, [dismissCreatedWebdavToken, navigate]);
+
+ useEffect(() => {
+ const handleKeyDown = (event) => {
+ if (event.key === 'Escape') {
+ event.preventDefault();
+ handleClose();
+ }
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [handleClose]);
+
+ useEffect(() => () => {
+ dismissCreatedWebdavToken();
+ }, [dismissCreatedWebdavToken]);
+
+ return (
+
+ );
+};
+
+export default SettingsRoute;
diff --git a/frontend/src/app/useManagementModals.js b/frontend/src/app/useManagementModals.js
new file mode 100644
index 0000000..e9e7650
--- /dev/null
+++ b/frontend/src/app/useManagementModals.js
@@ -0,0 +1,219 @@
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import TagsPanel from '../tags/TagsPanel';
+import CorrespondentsPanel from '../correspondents/CorrespondentsPanel';
+import DocumentTypesPanel from '../documentTypes/DocumentTypesPanel';
+
+const TAGS_MODAL = 'tags';
+const CORRESPONDENTS_MODAL = 'correspondents';
+const DOCUMENT_TYPES_MODAL = 'document-types';
+
+export const useManagementModals = ({
+ locationPathname,
+ tags,
+ refreshTags,
+ onTagCreate,
+ onTagUpdate,
+ onTagDelete,
+ correspondents,
+ refreshCorrespondents,
+ onCorrespondentCreate,
+ onCorrespondentUpdate,
+ onCorrespondentDelete,
+ documentTypes,
+ refreshDocumentTypes,
+ onDocumentTypeCreate,
+ onDocumentTypeUpdate,
+ onDocumentTypeDelete,
+ setStatusMessage,
+}) => {
+ const [activeModal, setActiveModal] = useState(null);
+
+ const openTagsModal = useCallback(() => setActiveModal(TAGS_MODAL), []);
+ const openCorrespondentsModal = useCallback(
+ () => setActiveModal(CORRESPONDENTS_MODAL),
+ [],
+ );
+ const openDocumentTypesModal = useCallback(
+ () => setActiveModal(DOCUMENT_TYPES_MODAL),
+ [],
+ );
+ const closeActiveModal = useCallback(() => setActiveModal(null), []);
+
+ useEffect(() => {
+ setActiveModal(null);
+ }, [locationPathname]);
+
+ useEffect(() => {
+ if (!activeModal) {
+ return undefined;
+ }
+ const handleKeyDown = (event) => {
+ if (event.key === 'Escape') {
+ event.preventDefault();
+ setActiveModal(null);
+ }
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [activeModal]);
+
+ const tagModal = useMemo(() => {
+ if (activeModal !== TAGS_MODAL) {
+ return null;
+ }
+ return (
+
+
event.stopPropagation()}
+ >
+
+
+
+
+
+
+
+
+
+ );
+ }, [
+ activeModal,
+ closeActiveModal,
+ onTagCreate,
+ onTagDelete,
+ onTagUpdate,
+ refreshTags,
+ setStatusMessage,
+ tags,
+ ]);
+
+ const correspondentsModal = useMemo(() => {
+ if (activeModal !== CORRESPONDENTS_MODAL) {
+ return null;
+ }
+ return (
+
+
event.stopPropagation()}
+ >
+
+
Manage Correspondents
+
+
+
+
+
+
+
+ );
+ }, [
+ activeModal,
+ closeActiveModal,
+ correspondents,
+ onCorrespondentCreate,
+ onCorrespondentDelete,
+ onCorrespondentUpdate,
+ refreshCorrespondents,
+ setStatusMessage,
+ ]);
+
+ const documentTypesModal = useMemo(() => {
+ if (activeModal !== DOCUMENT_TYPES_MODAL) {
+ return null;
+ }
+ return (
+
+
event.stopPropagation()}
+ >
+
+
Manage Document Types
+
+
+
+
+
+
+
+ );
+ }, [
+ activeModal,
+ closeActiveModal,
+ documentTypes,
+ onDocumentTypeCreate,
+ onDocumentTypeDelete,
+ onDocumentTypeUpdate,
+ refreshDocumentTypes,
+ setStatusMessage,
+ ]);
+
+ const managementModals = (
+ <>
+ {tagModal}
+ {correspondentsModal}
+ {documentTypesModal}
+ >
+ );
+
+ return {
+ managementModals,
+ openTagsModal,
+ openCorrespondentsModal,
+ openDocumentTypesModal,
+ closeActiveModal,
+ activeModal,
+ };
+};
+
+export default useManagementModals;
diff --git a/frontend/src/app/useWorkspaceSurface.js b/frontend/src/app/useWorkspaceSurface.js
new file mode 100644
index 0000000..d2259a7
--- /dev/null
+++ b/frontend/src/app/useWorkspaceSurface.js
@@ -0,0 +1,118 @@
+import React, { useCallback, useMemo } from 'react';
+import { ChevronsRightIcon } from '../ui/icons';
+import { createDocumentsSurface } from '../documents/DocumentsTable';
+import { createPreviewSurface } from '../preview/PreviewWorkspace';
+import { createDesktopSurface } from '../DesktopWorkspace';
+
+export const useWorkspaceSurface = ({
+ sidebarCollapsed,
+ onExpandSidebar,
+ documentsTableProps,
+ detailPanelProps,
+ detailPanelOpen,
+ workspaceMode,
+ skeuoWorkspaceProps,
+ previewWorkspaceDocument,
+ previewWorkspaceEntry,
+ ensureAssetUrl,
+ ensurePreviewData,
+ getDocumentAsset,
+ resolveApiPath,
+ notifyApiError,
+ handleThumbnailRegeneration,
+ closeDocumentPreview,
+ parentBreadcrumb,
+ onNavigateParent,
+}) => {
+ const renderSidebarToggle = useCallback(() => {
+ if (!sidebarCollapsed) {
+ return null;
+ }
+ return (
+
+ );
+ }, [sidebarCollapsed, onExpandSidebar]);
+
+ const documentsSurface = useMemo(() => {
+ if (!documentsTableProps) {
+ return null;
+ }
+ return createDocumentsSurface({
+ tableProps: documentsTableProps,
+ parentBreadcrumb,
+ onNavigateParent: parentBreadcrumb ? onNavigateParent : null,
+ renderSidebarToggle,
+ detailProps: detailPanelProps,
+ detailOpen: detailPanelOpen,
+ });
+ }, [
+ documentsTableProps,
+ parentBreadcrumb,
+ onNavigateParent,
+ renderSidebarToggle,
+ detailPanelProps,
+ detailPanelOpen,
+ ]);
+
+ const showPreviewWorkspace = workspaceMode !== 'skeuo' && Boolean(previewWorkspaceDocument);
+
+ const previewSurface = useMemo(() => {
+ if (!showPreviewWorkspace || !previewWorkspaceDocument) {
+ return null;
+ }
+ return createPreviewSurface({
+ document: previewWorkspaceDocument,
+ previewEntry: previewWorkspaceEntry,
+ ensureAssetUrl,
+ ensurePreviewData,
+ getDocumentAsset,
+ resolveApiPath,
+ notifyApiError,
+ onRegenerate: handleThumbnailRegeneration,
+ onClose: closeDocumentPreview,
+ renderSidebarToggle,
+ });
+ }, [
+ showPreviewWorkspace,
+ previewWorkspaceDocument,
+ previewWorkspaceEntry,
+ ensureAssetUrl,
+ ensurePreviewData,
+ getDocumentAsset,
+ resolveApiPath,
+ notifyApiError,
+ handleThumbnailRegeneration,
+ closeDocumentPreview,
+ renderSidebarToggle,
+ ]);
+
+ const workspaceSurface = useMemo(() => {
+ if (workspaceMode !== 'skeuo') {
+ return null;
+ }
+ return createDesktopSurface({
+ workspaceProps: skeuoWorkspaceProps,
+ renderSidebarToggle,
+ });
+ }, [workspaceMode, skeuoWorkspaceProps, renderSidebarToggle]);
+
+ const surface = useMemo(() => {
+ if (showPreviewWorkspace) {
+ return previewSurface;
+ }
+ if (workspaceMode === 'skeuo') {
+ return workspaceSurface;
+ }
+ return documentsSurface;
+ }, [showPreviewWorkspace, workspaceMode, previewSurface, workspaceSurface, documentsSurface]);
+
+ return { surface };
+};