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"
/>
+