diff --git a/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.tsx b/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.tsx index be9bbcf5..a0ed28cf 100644 --- a/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.tsx +++ b/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.tsx @@ -9,13 +9,13 @@ import { IconButton, } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; -import React, { useMemo } from 'react'; +import React, { ReactNode, useMemo } from 'react'; export interface IConfirmationDialog { isOpen: boolean; onCloseDialog: (confirm: boolean | undefined) => void; dialogTitle: string; - dialogMessage?: JSX.Element | string; + dialogMessage?: ReactNode | string; confirmText?: string; rejectText?: string; } diff --git a/compose/neurosynth-frontend/src/components/NeurosynthBreadcrumbs.tsx b/compose/neurosynth-frontend/src/components/NeurosynthBreadcrumbs.tsx index 01491116..ecc7d5f3 100644 --- a/compose/neurosynth-frontend/src/components/NeurosynthBreadcrumbs.tsx +++ b/compose/neurosynth-frontend/src/components/NeurosynthBreadcrumbs.tsx @@ -1,6 +1,8 @@ import { Box, Breadcrumbs, Link, Typography } from '@mui/material'; -import React from 'react'; -import { NavLink } from 'react-router-dom'; +import React, { useState } from 'react'; +import { NavLink, useNavigate } from 'react-router-dom'; +import ConfirmationDialog from './Dialogs/ConfirmationDialog'; +import { hasUnsavedChanges, hasUnsavedStudyChanges } from 'helpers/BeforeUnload.helpers'; interface INeurosynthBreadcrumbs { link: string; @@ -10,8 +12,46 @@ interface INeurosynthBreadcrumbs { const NeurosynthBreadcrumbs: React.FC<{ breadcrumbItems: INeurosynthBreadcrumbs[] }> = React.memo( (props) => { + const [confirmationDialogState, setConfirmationDialogState] = useState({ + isOpen: false, + navigationLink: '', + }); + const navigate = useNavigate(); + + const handleNavigate = (link: string) => { + const hasUnsavedChanges = hasUnsavedStudyChanges(); + if (hasUnsavedChanges) { + setConfirmationDialogState({ + isOpen: true, + navigationLink: link, + }); + } else { + navigate(link); + } + }; + + const handleCloseConfirmationDialog = (ok: boolean | undefined) => { + if (ok) { + navigate(confirmationDialogState.navigationLink); + } + + setConfirmationDialogState({ + isOpen: false, + navigationLink: '', + }); + }; + return ( + + {props.breadcrumbItems.map((breadcrumb, index) => breadcrumb.isCurrentPage ? ( @@ -34,6 +74,10 @@ const NeurosynthBreadcrumbs: React.FC<{ breadcrumbItems: INeurosynthBreadcrumbs[ key={index} component={NavLink} to={breadcrumb.link} + onClick={(e) => { + e.preventDefault(); + handleNavigate(breadcrumb.link); + }} sx={{ fontSize: '1.25rem', cursor: 'pointer', diff --git a/compose/neurosynth-frontend/src/pages/Extraction/ExtractionPage.tsx b/compose/neurosynth-frontend/src/pages/Extraction/ExtractionPage.tsx index 395d3593..0b487b33 100644 --- a/compose/neurosynth-frontend/src/pages/Extraction/ExtractionPage.tsx +++ b/compose/neurosynth-frontend/src/pages/Extraction/ExtractionPage.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Typography } from '@mui/material'; +import { Box, Button, Tooltip, Typography } from '@mui/material'; import NeurosynthBreadcrumbs from 'components/NeurosynthBreadcrumbs'; import ProjectIsLoadingText from 'components/ProjectIsLoadingText'; import StateHandlerComponent from 'components/StateHandlerComponent/StateHandlerComponent'; @@ -86,17 +86,13 @@ const ExtractionPage: React.FC = (props) => { }; const handleMoveToSpecificationPhase = () => { - if (canEditMetaAnalyses) { - navigate(`/projects/${projectId}/meta-analyses`); - } else { - navigate(`/projects/${projectId}/project`, { - state: { - projectPage: { - scrollToMetaAnalysisProceed: true, - }, - } as IProjectPageLocationState, - }); - } + navigate(`/projects/${projectId}/project`, { + state: { + projectPage: { + scrollToMetaAnalysisProceed: true, + }, + } as IProjectPageLocationState, + }); }; const isReadyToMoveToNextStep = useMemo( @@ -105,6 +101,17 @@ const ExtractionPage: React.FC = (props) => { [extractionSummary] ); + const percentageCompleteString = useMemo((): string => { + if (extractionSummary.total === 0) return '0 / 0'; + return `${extractionSummary.completed} / ${extractionSummary.total}`; + }, [extractionSummary.completed, extractionSummary.total]); + + const percentageComplete = useMemo((): number => { + if (extractionSummary.total === 0) return 0; + const percentageComplete = (extractionSummary.completed / extractionSummary.total) * 100; + return Math.floor(percentageComplete); + }, [extractionSummary.completed, extractionSummary.total]); + return ( @@ -133,28 +140,43 @@ const ExtractionPage: React.FC = (props) => { - {isReadyToMoveToNextStep && ( - - )} + + + + + + {showReconcilePrompt && } diff --git a/compose/neurosynth-frontend/src/pages/Project/components/ProjectExtractionStepCard.tsx b/compose/neurosynth-frontend/src/pages/Project/components/ProjectExtractionStepCard.tsx index a27442e6..5acb968e 100644 --- a/compose/neurosynth-frontend/src/pages/Project/components/ProjectExtractionStepCard.tsx +++ b/compose/neurosynth-frontend/src/pages/Project/components/ProjectExtractionStepCard.tsx @@ -141,7 +141,7 @@ const ProjectExtractionStepCard: React.FC<{ disabled: boolean }> = ({ disabled } display: allStudiesAreComplete ? 'none' : 'block', }} onClick={() => setMarkAllAsCompleteConfirmationDialogIsOpen(true)} - color="success" + color="info" disabled={disabled} > Mark all as complete diff --git a/compose/neurosynth-frontend/src/pages/Project/components/ProjectViewMetaAnalyses.tsx b/compose/neurosynth-frontend/src/pages/Project/components/ProjectViewMetaAnalyses.tsx index 5692d6e2..fdea32ba 100644 --- a/compose/neurosynth-frontend/src/pages/Project/components/ProjectViewMetaAnalyses.tsx +++ b/compose/neurosynth-frontend/src/pages/Project/components/ProjectViewMetaAnalyses.tsx @@ -1,21 +1,22 @@ import { Add } from '@mui/icons-material'; import { Box, Button, Typography } from '@mui/material'; +import CreateMetaAnalysisSpecificationDialogBase from 'pages/MetaAnalysis/components/CreateMetaAnalysisSpecificationDialogBase'; import StateHandlerComponent from 'components/StateHandlerComponent/StateHandlerComponent'; import { useGetMetaAnalysesByIds, useGuard } from 'hooks'; -import useUserCanEdit from 'hooks/useUserCanEdit'; -import { MetaAnalysisReturn } from 'neurosynth-compose-typescript-sdk'; -import CreateMetaAnalysisSpecificationDialogBase from 'pages/MetaAnalysis/components/CreateMetaAnalysisSpecificationDialogBase'; -import ProjectViewMetaAnalysis from 'pages/Project/components/ProjectViewMetaAnalysis'; import { useProjectId, useProjectMetaAnalyses, useProjectMetaAnalysisCanEdit, useProjectUser, } from 'pages/Project/store/ProjectStore'; +import { MetaAnalysisReturn } from 'neurosynth-compose-typescript-sdk'; import { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import ProjectViewMetaAnalysis from 'pages/Project/components/ProjectViewMetaAnalysis'; +import useUserCanEdit from 'hooks/useUserCanEdit'; const ProjectViewMetaAnalyses: React.FC = () => { - const projectId = useProjectId(); + const { projectId } = useParams<{ projectId: string }>(); const projectUser = useProjectUser(); const canEdit = useUserCanEdit(projectUser || undefined); const projectMetaAnalyses = useProjectMetaAnalyses() || []; @@ -31,12 +32,15 @@ const ProjectViewMetaAnalyses: React.FC = () => { } const { data = [], isLoading, isError } = useGetMetaAnalysesByIds(metaAnalysisIds); const canEditMetaAnalyses = useProjectMetaAnalysisCanEdit(); + const projectIdFromProject = useProjectId(); const [createMetaAnalysisDialogIsOpen, setCreateMetaAnalysisDialogIsOpen] = useState(false); useGuard( - `/projects/${projectId}/edit`, - 'you must finish the meta-analysis creation process to view this page', - projectId !== undefined ? false : !canEditMetaAnalyses + `/projects/${projectId}/project`, + 'You must finish the meta-analysis creation process to view this page', + projectIdFromProject === undefined || projectId !== projectIdFromProject + ? false + : !canEditMetaAnalyses ); return ( diff --git a/compose/neurosynth-frontend/src/pages/Study/EditStudyPage.tsx b/compose/neurosynth-frontend/src/pages/Study/EditStudyPage.tsx index aee488be..7509af29 100644 --- a/compose/neurosynth-frontend/src/pages/Study/EditStudyPage.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/EditStudyPage.tsx @@ -1,5 +1,7 @@ import { Box, Button } from '@mui/material'; +import ConfirmationDialog from 'components/Dialogs/ConfirmationDialog'; import StateHandlerComponent from 'components/StateHandlerComponent/StateHandlerComponent'; +import { hasUnsavedStudyChanges, unsetUnloadHandler } from 'helpers/BeforeUnload.helpers'; import { useInitProjectStoreIfRequired, useProjectExtractionAnnotationId, @@ -17,13 +19,11 @@ import { useStudyId, } from 'pages/Study/store/StudyStore'; import { useEffect, useState } from 'react'; -import { Link, useNavigate, useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { useClearAnnotationStore, useInitAnnotationStore } from 'stores/AnnotationStore.actions'; import { useAnnotationId, useGetAnnotationIsLoading } from 'stores/AnnotationStore.getters'; import DisplayExtractionTableState from './components/DisplayExtractionTableState'; import EditStudyCompleteButton from './components/EditStudyCompleteButton'; -import ConfirmationDialog from 'components/Dialogs/ConfirmationDialog'; -import { hasUnsavedStudyChanges, unsetUnloadHandler } from 'helpers/BeforeUnload.helpers'; const EditStudyPage: React.FC = (props) => { const { projectId, studyId } = useParams<{ projectId: string; studyId: string }>(); diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyPageHeader.tsx b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyPageHeader.tsx index 49b7f399..16073c78 100644 --- a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyPageHeader.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyPageHeader.tsx @@ -1,21 +1,19 @@ -import { Box, Button, Typography } from '@mui/material'; +import { Box, Typography } from '@mui/material'; import DisplayStudyChipLinks from 'components/DisplayStudyChipLinks/DisplayStudyChipLinks'; -import EditStudyToolbar from 'pages/Study/components/EditStudyToolbar'; import NeurosynthBreadcrumbs from 'components/NeurosynthBreadcrumbs'; import ProjectIsLoadingText from 'components/ProjectIsLoadingText'; import { useProjectId, useProjectName } from 'pages/Project/store/ProjectStore'; +import EditStudyToolbar from 'pages/Study/components/EditStudyToolbar'; import { - useStudyId, + useStudyAuthors, useStudyLastUpdated, useStudyName, - useStudyYear, - useStudyAuthors, useStudyUsername, + useStudyYear, } from 'pages/Study/store/StudyStore'; import { useMemo } from 'react'; const EditStudyPageHeader: React.FC = () => { - const studyId = useStudyId(); const projectId = useProjectId(); const studyName = useStudyName(); const studyYear = useStudyYear(); @@ -53,7 +51,7 @@ const EditStudyPageHeader: React.FC = () => { }, { text: studyName || '', - link: `/projects/${projectId}/extraction/studies/${studyId}/edit`, + link: '', isCurrentPage: true, }, ]} diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudySwapVersionButton.tsx b/compose/neurosynth-frontend/src/pages/Study/components/EditStudySwapVersionButton.tsx index 527760b8..598e1b3e 100644 --- a/compose/neurosynth-frontend/src/pages/Study/components/EditStudySwapVersionButton.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/components/EditStudySwapVersionButton.tsx @@ -4,7 +4,6 @@ import { Box, Button, ButtonGroup, - CircularProgress, ListItem, ListItemButton, Menu, @@ -12,7 +11,9 @@ import { Typography, } from '@mui/material'; import ConfirmationDialog from 'components/Dialogs/ConfirmationDialog'; +import ProgressLoader from 'components/ProgressLoader'; import { setAnalysesInAnnotationAsIncluded } from 'helpers/Annotation.helpers'; +import { hasUnsavedStudyChanges, unsetUnloadHandler } from 'helpers/BeforeUnload.helpers'; import { lastUpdatedAtSortFn } from 'helpers/utils'; import { useGetStudysetById, useUpdateStudyset } from 'hooks'; import useGetBaseStudyById from 'hooks/studies/useGetBaseStudyById'; @@ -25,7 +26,7 @@ import { useProjectId, } from 'pages/Project/store/ProjectStore'; import { useStudyBaseStudyId, useStudyId } from 'pages/Study/store/StudyStore'; -import { useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAnnotationId } from 'stores/AnnotationStore.getters'; @@ -46,6 +47,7 @@ const EditStudySwapVersionButton: React.FC = (props) => { const annotationId = useAnnotationId(); const [isSwapping, setIsSwapping] = useState(false); + const [unsavedChangesConfirmationDialog, setUnsavedChangesConfirmationDialog] = useState(false); const [confirmationDialogState, setConfirmationDialogState] = useState<{ isOpen: boolean; selectedVersion?: string; @@ -66,13 +68,22 @@ const EditStudySwapVersionButton: React.FC = (props) => { if (confirm) { handleSwapStudy(confirmationDialogState.selectedVersion); } - setConfirmationDialogState((prev) => ({ - ...prev, + setConfirmationDialogState({ isOpen: false, selectedVersion: undefined, - })); + }); }; + /** + * Handle swapping the current study being edited with another version. + * The selected version is confirmed by the user in a confirmation dialog. + * If confirmed, the studyset is updated to replace the current study with the selected version. + * The studylist status is updated to reflect the new study version. + * The extraction table state in storage is updated to point to the new study version. + * The analyses in the annotation are set to be included. + * The user is redirected to the edit page of the new study version. + * @param {string} versionToSwapTo - the id of the version to swap to + */ const handleSwapStudy = async (versionToSwapTo?: string) => { if (!annotationId || !studyId || !studysetId || !versionToSwapTo || !studyset?.studies) return; @@ -114,16 +125,35 @@ const EditStudySwapVersionButton: React.FC = (props) => { } }; - const handleSelectVersion = (versionId: string | undefined) => { + const handleCloseUnsavedChangesDialog = (ok: boolean | undefined) => { + if (ok) { + unsetUnloadHandler('study'); + unsetUnloadHandler('annotation'); + } + setUnsavedChangesConfirmationDialog(false); + handleCloseConfirmationDialog(ok); + }; + + const handleUnsavedChanges = (ok: boolean | undefined) => { + if (ok) { + const hasUnsavedChanges = hasUnsavedStudyChanges(); + if (hasUnsavedChanges) { + setConfirmationDialogState((prev) => ({ ...prev, isOpen: false })); + setUnsavedChangesConfirmationDialog(true); + return; + } + } + handleCloseConfirmationDialog(ok); + }; + + const handleSwitchVersion = (versionId: string | undefined) => { if (!versionId) return; if (versionId === studyId) { handleCloseNavMenu(); return; } - setConfirmationDialogState({ - isOpen: true, - selectedVersion: versionId, - }); + + setConfirmationDialogState({ isOpen: true, selectedVersion: versionId }); }; const baseStudyVersions = useMemo(() => { @@ -141,9 +171,19 @@ const EditStudySwapVersionButton: React.FC = (props) => { onClick={handleButtonPress} size="small" variant="outlined" - sx={{ width: '40px', minWidth: '40px', height: '40px' }} + sx={{ + width: '40px', + maxWidth: '40px', + minWidth: '40px', + height: '40px', + padding: 0, + }} > - {isSwapping ? : } + {isSwapping ? ( + + ) : ( + + )} @@ -152,7 +192,7 @@ const EditStudySwapVersionButton: React.FC = (props) => { dialogMessage={ <> - You are switching from version {studyId} to version + You are switching from version {studyId} to version{' '} {confirmationDialogState.selectedVersion || ''} @@ -161,10 +201,17 @@ const EditStudySwapVersionButton: React.FC = (props) => { } - onCloseDialog={handleCloseConfirmationDialog} + onCloseDialog={handleUnsavedChanges} isOpen={confirmationDialogState.isOpen} rejectText="Cancel" /> + { anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }} transformOrigin={{ vertical: 'bottom', horizontal: 'right' }} > - {baseStudyVersions.map((baseStudyVersion) => { - const isCurrentlySelected = baseStudyVersion.id === studyId; - const username = baseStudyVersion.username - ? baseStudyVersion.username - : 'neurosynth'; + {baseStudyVersions.map((version) => { + const isCurrentlySelected = version.id === studyId; + const username = version.username ? version.username : 'neurosynth'; + const lastUpdated = new Date( + version.updated_at || version.created_at || '' + ).toLocaleString(); return ( - +