From 3c5f1aee17bc03b632bb7c877ad50f1974071da9 Mon Sep 17 00:00:00 2001 From: Nicholas Lee Date: Fri, 4 Oct 2024 15:02:53 -0400 Subject: [PATCH] feat: finished edit study page --- .../Dialogs/ConfirmationDialog.spec.tsx | 1 - .../components/Dialogs/ConfirmationDialog.tsx | 11 +- .../components/NeurosynthConfirmationChip.tsx | 2 +- .../src/helpers/BeforeUnload.helpers.ts | 39 +- .../src/hooks/external/useGetFullText.tsx | 4 +- .../components/ExtractionTable.helpers.ts | 44 +++ .../Extraction/components/ExtractionTable.tsx | 28 +- .../components/ProjectViewMetaAnalyses.tsx | 11 +- .../src/pages/Study/EditStudyPage.tsx | 123 +++--- .../DisplayExtractionTableState.tsx | 279 ++++++++++--- .../components/EditStudyCompleteButton.tsx | 54 +++ .../components/EditStudySwapVersionButton.tsx | 48 ++- .../Study/components/EditStudyToolbar.tsx | 368 +++++++++--------- .../useSaveStudy.helpers.ts} | 0 .../useSaveStudy.tsx} | 83 ++-- .../src/stores/AnnotationStore.ts | 1 - 16 files changed, 679 insertions(+), 417 deletions(-) create mode 100644 compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTable.helpers.ts create mode 100644 compose/neurosynth-frontend/src/pages/Study/components/EditStudyCompleteButton.tsx rename compose/neurosynth-frontend/src/pages/Study/{components/EditStudySaveButton.helpers.ts => hooks/useSaveStudy.helpers.ts} (100%) rename compose/neurosynth-frontend/src/pages/Study/{components/EditStudySaveButton.tsx => hooks/useSaveStudy.tsx} (90%) diff --git a/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.spec.tsx b/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.spec.tsx index 4467c7dee..6b79c70e2 100644 --- a/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.spec.tsx +++ b/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.spec.tsx @@ -103,7 +103,6 @@ describe('ConfirmationDialog', () => { onCloseDialog={mockOnClose} confirmText="confirm" rejectText="reject" - data={{ data: 'test-data' }} /> ); diff --git a/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.tsx b/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.tsx index 3fa9b23a7..be9bbcf51 100644 --- a/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.tsx +++ b/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.tsx @@ -13,12 +13,11 @@ import React, { useMemo } from 'react'; export interface IConfirmationDialog { isOpen: boolean; - onCloseDialog: (confirm: boolean | undefined, data?: any) => void; + onCloseDialog: (confirm: boolean | undefined) => void; dialogTitle: string; dialogMessage?: JSX.Element | string; confirmText?: string; rejectText?: string; - data?: any; } const ConfirmationDialog: React.FC = (props) => { @@ -33,13 +32,13 @@ const ConfirmationDialog: React.FC = (props) => { }, [props.dialogMessage]); return ( - props.onCloseDialog(undefined, props.data)}> + props.onCloseDialog(undefined)}> {props.dialogTitle} - props.onCloseDialog(undefined, props.data)}> + props.onCloseDialog(undefined)}> @@ -49,7 +48,7 @@ const ConfirmationDialog: React.FC = (props) => { - - - 2 of 3 - - - - {/* - */} - - + + + diff --git a/compose/neurosynth-frontend/src/pages/Study/components/DisplayExtractionTableState.tsx b/compose/neurosynth-frontend/src/pages/Study/components/DisplayExtractionTableState.tsx index fb4f1cc7d..fe88e6756 100644 --- a/compose/neurosynth-frontend/src/pages/Study/components/DisplayExtractionTableState.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/components/DisplayExtractionTableState.tsx @@ -1,70 +1,233 @@ -import { Box, Chip, Typography } from '@mui/material'; -import { ColumnFiltersState, SortingState } from '@tanstack/react-table'; -import { useGetStudysetById } from 'hooks'; -import { useProjectExtractionStudysetId, useProjectId } from 'pages/Project/store/ProjectStore'; -import { useMemo } from 'react'; +import { ArrowLeft, ArrowRight } from '@mui/icons-material'; +import { Box, Button, Tooltip, Typography } from '@mui/material'; +import { useGetStudyById, useGetStudysetById, useUserCanEdit } from 'hooks'; +import { retrieveExtractionTableState } from 'pages/Extraction/components/ExtractionTable.helpers'; +import { + useProjectExtractionStudysetId, + useProjectId, + useProjectUser, +} from 'pages/Project/store/ProjectStore'; +import { useStudyId } from '../store/StudyStore'; +import { hasUnsavedStudyChanges, unsetUnloadHandler } from 'helpers/BeforeUnload.helpers'; +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import ConfirmationDialog from 'components/Dialogs/ConfirmationDialog'; const DisplayExtractionTableState: React.FC = (props) => { const projectId = useProjectId(); + const studyId = useStudyId(); const studysetId = useProjectExtractionStudysetId(); - const { data } = useGetStudysetById(studysetId); - const { columnFilters, sorting, studies } = useMemo(() => { - try { - const state = window.sessionStorage.getItem(`${projectId}-extraction-table`); - const parsedState = JSON.parse(state || '{}') as { - columnFilters: ColumnFiltersState; - sorting: SortingState; - studies: string[]; - }; - if (!state) { - return { - columnFilters: [], - sorting: [], - studies: [], - }; + const { data } = useGetStudysetById(studysetId, false); + const extractionTableState = retrieveExtractionTableState(projectId); + const thisStudyIndex = (extractionTableState?.studies || []).indexOf(studyId || ''); + const prevStudyId = extractionTableState?.studies[thisStudyIndex - 1]; + const nextStudyId = extractionTableState?.studies[thisStudyIndex + 1]; + + const { data: prevStudy, isLoading: prevStudyIsLoading } = useGetStudyById(prevStudyId); + const { data: nextStudy, isLoading: nextStudyIsLoading } = useGetStudyById(nextStudyId); + + const [confirmationDialogState, setConfirmationDialogState] = useState<{ + isOpen: boolean; + action: 'PREV' | 'NEXT' | undefined; + }>({ + isOpen: false, + action: undefined, + }); + + const navigate = useNavigate(); + + const user = useProjectUser(); + const canEdit = useUserCanEdit(user ?? undefined); + + const handleMoveToPreviousStudy = () => { + if (!prevStudyId) throw new Error('no previous study'); + + const hasUnsavedChanges = hasUnsavedStudyChanges(); + if (hasUnsavedChanges) { + setConfirmationDialogState({ + isOpen: true, + action: 'PREV', + }); + return; + } + + canEdit + ? navigate(`/projects/${projectId}/extraction/studies/${prevStudyId}/edit`) + : navigate(`/projects/${projectId}/extraction/studies/${prevStudyId}`); + }; + + const handleMoveToNextStudy = () => { + if (!nextStudyId) throw new Error('no next study'); + + const hasUnsavedChanges = hasUnsavedStudyChanges(); + if (hasUnsavedChanges) { + setConfirmationDialogState({ + isOpen: true, + action: 'NEXT', + }); + return; + } + + canEdit + ? navigate(`/projects/${projectId}/extraction/studies/${nextStudyId}/edit`) + : navigate(`/projects/${projectId}/extraction/studies/${nextStudyId}`); + }; + + const handleConfirmationDialogClose = (ok: boolean | undefined) => { + if (!ok) { + setConfirmationDialogState({ + isOpen: false, + action: undefined, + }); + } else { + unsetUnloadHandler('study'); + unsetUnloadHandler('annotation'); + switch (confirmationDialogState.action) { + case 'PREV': + handleMoveToPreviousStudy(); + break; + case 'NEXT': + handleMoveToNextStudy(); + break; } + setConfirmationDialogState({ + isOpen: false, + action: undefined, + }); + } + }; + + const filterStr = (extractionTableState?.columnFilters || []).reduce((acc, curr, index) => { + if (index === 0) { + return `Filtering by: ${curr.id}: ${curr.value}`; + } + return `${acc}, ${curr.id}: ${curr.value}`; + }, ''); - return { - columnFilters: parsedState.columnFilters, - sorting: parsedState.sorting, - studies: parsedState.studies, - }; - } catch (e) { - return { - columnFilters: [], - sorting: [], - studies: [], - }; + const sortingStr = (extractionTableState?.sorting || []).reduce((acc, curr, index) => { + if (index === 0) { + return `Sorting by ${curr.id}: ${curr.desc ? 'desc' : 'asc'}`; } - }, [projectId]); + return `${acc}, ${curr.id}: ${curr.desc ? 'desc' : 'asc'}`; + }, ''); return ( - - {columnFilters - .filter((filter) => !!filter.value) - .map((filter) => ( - - ))} - {sorting.map((sort) => ( - - ))} - - ({studies.length} / {data?.studies?.length || 0} studies) - + + + {prevStudyId ? ( + + + + ) : ( + + )} + + {filterStr && ( + + {filterStr} + + )} + {sortingStr && ( + + {sortingStr} + + )} + + ) + } + > + + + {thisStudyIndex + 1} of {(extractionTableState?.studies || []).length} + + ({data?.studies?.length || 0} total) + + + + {(extractionTableState?.columnFilters || []).length > 0 && ( + <>{(extractionTableState?.columnFilters || []).length} filters + )} + {(extractionTableState?.sorting || []).map((sorting) => ( + <> + {(extractionTableState?.columnFilters || []).length > 0 ? ', ' : ''} + sorting by {sorting.id} + + ))} + + + + {nextStudyId ? ( + + + + ) : ( + + )} ); }; diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyCompleteButton.tsx b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyCompleteButton.tsx new file mode 100644 index 000000000..837c9fbaf --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyCompleteButton.tsx @@ -0,0 +1,54 @@ +import { Box, Button } from '@mui/material'; +import { EExtractionStatus } from 'pages/Extraction/ExtractionPage'; +import { + useProjectExtractionAddOrUpdateStudyListStatus, + useProjectExtractionStudyStatus, +} from 'pages/Project/store/ProjectStore'; +import React from 'react'; +import { useParams } from 'react-router-dom'; +import useSaveStudy from '../hooks/useSaveStudy'; +import LoadingButton from 'components/Buttons/LoadingButton'; + +const EditStudyCompleteButton: React.FC = React.memo((props) => { + const { studyId } = useParams<{ studyId: string }>(); + const { isLoading, hasEdits, handleSave } = useSaveStudy(); + const extractionStatus = useProjectExtractionStudyStatus(studyId || ''); + const updateStudyListStatus = useProjectExtractionAddOrUpdateStudyListStatus(); + + const handleSaveAndComplete = async () => { + let clonedId: string | undefined; + if (hasEdits) { + clonedId = await handleSave(); // this will only save if there are changes + } + if (extractionStatus?.status !== EExtractionStatus.COMPLETED) { + updateStudyListStatus(clonedId || studyId || '', EExtractionStatus.COMPLETED); + } + }; + + return ( + + + + ); +}); + +export default EditStudyCompleteButton; diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudySwapVersionButton.tsx b/compose/neurosynth-frontend/src/pages/Study/components/EditStudySwapVersionButton.tsx index 4713f12de..640a325b4 100644 --- a/compose/neurosynth-frontend/src/pages/Study/components/EditStudySwapVersionButton.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/components/EditStudySwapVersionButton.tsx @@ -1,7 +1,16 @@ import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import SwapHorizIcon from '@mui/icons-material/SwapHoriz'; -import { Button, ButtonGroup, ListItem, ListItemButton, Menu, Typography } from '@mui/material'; -import LoadingButton from 'components/Buttons/LoadingButton'; +import { + Box, + Button, + ButtonGroup, + CircularProgress, + ListItem, + ListItemButton, + Menu, + Tooltip, + Typography, +} from '@mui/material'; import ConfirmationDialog from 'components/Dialogs/ConfirmationDialog'; import { setAnalysesInAnnotationAsIncluded } from 'helpers/Annotation.helpers'; import { lastUpdatedAtSortFn } from 'helpers/utils'; @@ -9,6 +18,7 @@ import { useGetStudysetById, useUpdateStudyset } from 'hooks'; import useGetBaseStudyById from 'hooks/studies/useGetBaseStudyById'; import { StudyReturn } from 'neurostore-typescript-sdk'; import { useSnackbar } from 'notistack'; +import { updateExtractionTableStateInStorage } from 'pages/Extraction/components/ExtractionTable.helpers'; import { useProjectExtractionReplaceStudyListStatusId, useProjectExtractionStudysetId, @@ -88,6 +98,7 @@ const EditStudySwapVersionButton: React.FC = (props) => { }, }); updateStudyListStatusWithNewStudyId(studyId, versionToSwapTo); + updateExtractionTableStateInStorage(projectId, studyId, versionToSwapTo); await setAnalysesInAnnotationAsIncluded(annotationId); navigate(`/projects/${projectId}/extraction/studies/${versionToSwapTo}/edit`); @@ -122,16 +133,20 @@ const EditStudySwapVersionButton: React.FC = (props) => { return ( <> - } - text="Switch study version" - > + + + + + { open={open} onClose={handleCloseNavMenu} anchorEl={anchorEl} - anchorOrigin={{ vertical: 'top', horizontal: 'left' }} - transformOrigin={{ vertical: 'bottom', horizontal: 'left' }} + anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }} + transformOrigin={{ vertical: 'bottom', horizontal: 'right' }} > {baseStudyVersions.map((baseStudyVersion) => { const isCurrentlySelected = baseStudyVersion.id === studyId; @@ -165,7 +180,10 @@ const EditStudySwapVersionButton: React.FC = (props) => { return ( - + - - - - {/* - */} + - - {/* - */} + + + + - - - - - - {/* - - + - - - - + - */} - {/* - - - + + + + - - - - - - - - - + + + + - - - - - */} + {/* need this box as a wrapper because tooltip will not act on a disabled element */} + + + + + + )} diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudySaveButton.helpers.ts b/compose/neurosynth-frontend/src/pages/Study/hooks/useSaveStudy.helpers.ts similarity index 100% rename from compose/neurosynth-frontend/src/pages/Study/components/EditStudySaveButton.helpers.ts rename to compose/neurosynth-frontend/src/pages/Study/hooks/useSaveStudy.helpers.ts diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudySaveButton.tsx b/compose/neurosynth-frontend/src/pages/Study/hooks/useSaveStudy.tsx similarity index 90% rename from compose/neurosynth-frontend/src/pages/Study/components/EditStudySaveButton.tsx rename to compose/neurosynth-frontend/src/pages/Study/hooks/useSaveStudy.tsx index 18170eef4..336c0b845 100644 --- a/compose/neurosynth-frontend/src/pages/Study/components/EditStudySaveButton.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/hooks/useSaveStudy.tsx @@ -1,14 +1,5 @@ -import LoadingButton from 'components/Buttons/LoadingButton'; -import { AnalysisReturn, StudyRequest } from 'neurostore-typescript-sdk'; -import { useSnackbar } from 'notistack'; -import { - useProjectExtractionAnnotationId, - useProjectExtractionReplaceStudyListStatusId, - useProjectExtractionStudysetId, - useProjectId, -} from 'pages/Project/store/ProjectStore'; - import { useAuth0 } from '@auth0/auth0-react'; +import { unsetUnloadHandler } from 'helpers/BeforeUnload.helpers'; import { useCreateStudy, useGetStudysetById, @@ -16,16 +7,15 @@ import { useUpdateStudyset, } from 'hooks'; import { STUDYSET_QUERY_STRING } from 'hooks/studysets/useGetStudysets'; +import { AnalysisReturn, StudyRequest } from 'neurostore-typescript-sdk'; +import { useSnackbar } from 'notistack'; import { - useStudy, - useStudyAnalyses, - useStudyHasBeenEdited, - useStudyUser, - useUpdateStudyInDB, - useUpdateStudyIsLoading, -} from 'pages/Study/store/StudyStore'; -import { storeAnalysesToStudyAnalyses } from 'pages/Study/store/StudyStore.helpers'; -import React, { useState } from 'react'; + useProjectExtractionAnnotationId, + useProjectExtractionReplaceStudyListStatusId, + useProjectExtractionStudysetId, + useProjectId, +} from 'pages/Project/store/ProjectStore'; +import { useState } from 'react'; import { useQueryClient } from 'react-query'; import { useNavigate } from 'react-router-dom'; import { useUpdateAnnotationInDB, useUpdateAnnotationNotes } from 'stores/AnnotationStore.actions'; @@ -36,11 +26,20 @@ import { } from 'stores/AnnotationStore.getters'; import { storeNotesToDBNotes } from 'stores/AnnotationStore.helpers'; import API from 'utils/api'; -import { arrayToMetadata } from './EditStudyMetadata'; -import { hasDuplicateStudyAnalysisNames, hasEmptyStudyPoints } from './EditStudySaveButton.helpers'; -import { unsetUnloadHandler } from 'helpers/BeforeUnload.helpers'; +import { arrayToMetadata } from '../components/EditStudyMetadata'; +import { + useStudy, + useStudyAnalyses, + useStudyHasBeenEdited, + useStudyUser, + useUpdateStudyInDB, + useUpdateStudyIsLoading, +} from '../store/StudyStore'; +import { storeAnalysesToStudyAnalyses } from '../store/StudyStore.helpers'; +import { hasDuplicateStudyAnalysisNames, hasEmptyStudyPoints } from './useSaveStudy.helpers'; +import { updateExtractionTableStateInStorage } from 'pages/Extraction/components/ExtractionTable.helpers'; -const EditStudySaveButton: React.FC = React.memo((props) => { +const useSaveStudy = () => { const { user } = useAuth0(); const queryClient = useQueryClient(); const { enqueueSnackbar } = useSnackbar(); @@ -60,7 +59,6 @@ const EditStudySaveButton: React.FC = React.memo((props) => { const updateStudyInDB = useUpdateStudyInDB(); // annotation stuff const updateAnnotationIsLoading = useUpdateAnnotationIsLoading(); - const annotationHasBeenEdited = useAnnotationIsEdited(); const notes = useAnnotationNotes(); const annotationIsEdited = useAnnotationIsEdited(); const updateAnnotationNotes = useUpdateAnnotationNotes(); @@ -133,14 +131,14 @@ const EditStudySaveButton: React.FC = React.memo((props) => { } }; - const handleUpdateDB = () => { + const handleUpdateDB = async () => { try { if (studyHasBeenEdited && annotationIsEdited) { - handleUpdateBothInDB(); + await handleUpdateBothInDB(); } else if (studyHasBeenEdited) { - handleUpdateStudyInDB(); + await handleUpdateStudyInDB(); } else if (annotationIsEdited) { - handleUpdateAnnotationInDB(); + await handleUpdateAnnotationInDB(); } } catch (e) { console.error(e); @@ -208,6 +206,7 @@ const EditStudySaveButton: React.FC = React.memo((props) => { // 4. update the project as this keeps track of completion status of studies replaceStudyWithNewClonedStudy(storeStudy.id, clonedStudyId); + updateExtractionTableStateInStorage(projectId, storeStudy.id, clonedStudyId); // 5. as this is a completely new study, that we've just created, the annotations are cleared. // We need to update the annotations with our latest changes, and associate newly created analyses with their corresponding analysis changes. @@ -241,6 +240,8 @@ const EditStudySaveButton: React.FC = React.memo((props) => { enqueueSnackbar('Saved successfully. You are now the owner of this study', { variant: 'success', }); + + return clonedStudyId; } catch (e) { enqueueSnackbar( 'We encountered an error saving your study. Please contact the neurosynth-compose team', @@ -269,28 +270,20 @@ const EditStudySaveButton: React.FC = React.memo((props) => { const currentUserOwnsThisStudy = (studyOwnerUser || null) === (user?.sub || undefined); if (currentUserOwnsThisStudy) { - handleUpdateDB(); + await handleUpdateDB(); } else { if (studyHasBeenEdited) { - handleClone(); + return await handleClone(); } else { - handleUpdateDB(); + await handleUpdateDB(); } } }; - return ( - - ); -}); + const isLoading = updateStudyIsLoading || updateAnnotationIsLoading || isCloning; + const hasEdits = studyHasBeenEdited || annotationIsEdited; + + return { isLoading, hasEdits, handleSave }; +}; -export default EditStudySaveButton; +export default useSaveStudy; diff --git a/compose/neurosynth-frontend/src/stores/AnnotationStore.ts b/compose/neurosynth-frontend/src/stores/AnnotationStore.ts index c9e4bbe62..723bccb15 100644 --- a/compose/neurosynth-frontend/src/stores/AnnotationStore.ts +++ b/compose/neurosynth-frontend/src/stores/AnnotationStore.ts @@ -152,7 +152,6 @@ export const useAnnotationStore = create< })); }, updateAnnotationNoteName: (note) => { - setUnloadHandler('annotation'); set((state) => ({ ...state, annotation: {