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()} - > -
-

Manage Tags

- -
-
- -
-
-
- )} - {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()} + > +
+

Manage Tags

+ +
+
+ +
+
+
+ ); + }, [ + 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 }; +};