Skip to content

Commit

Permalink
feat: added swap study button feature
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoalee committed Oct 29, 2023
1 parent aac0355 commit 579010b
Show file tree
Hide file tree
Showing 15 changed files with 233 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,29 @@ import {
IconButton,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import React, { useMemo } from 'react';

export interface IConfirmationDialog {
isOpen: boolean;
onCloseDialog: (confirm: boolean | undefined, data?: any) => void;
dialogTitle: string;
dialogMessage?: string;
dialogMessage?: JSX.Element | string;
confirmText?: string;
rejectText?: string;
data?: any;
}

const ConfirmationDialog: React.FC<IConfirmationDialog> = (props) => {
const dialogContent = useMemo(() => {
if (!props.dialogMessage) return undefined;

if (typeof props.dialogMessage === 'string') {
return <DialogContentText>{props.dialogMessage}</DialogContentText>;
} else {
return props.dialogMessage;
}
}, [props.dialogMessage]);

return (
<Dialog open={props.isOpen} onClose={() => props.onCloseDialog(undefined, props.data)}>
<DialogTitle sx={{ display: 'flex' }}>
Expand All @@ -34,11 +45,7 @@ const ConfirmationDialog: React.FC<IConfirmationDialog> = (props) => {
</Box>
</DialogTitle>
<DialogContent>
{props.dialogMessage && (
<DialogContentText sx={{ marginBottom: '1rem' }}>
{props.dialogMessage}
</DialogContentText>
)}
{props.dialogMessage && dialogContent}
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Button
sx={{ width: '250px', marginRight: '15px' }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
import { BaseStudy, StudyReturn } from 'neurostore-typescript-sdk';

export const lastUpdatedAtSortFn = (
a: { updated_at?: string | null; created_at?: string | null },
b: { updated_at?: string | null; created_at?: string | null }
): number => {
const dateAUpdatedAt = Date.parse(a.updated_at || '');
const dateBUpdatedAt = Date.parse(b.updated_at || '');

if (isNaN(dateAUpdatedAt) && dateBUpdatedAt) {
// if update_at A does not exist, automatically treat A as smaller
return -1;
} else if (isNaN(dateBUpdatedAt) && dateAUpdatedAt) {
// if update_at B does not exist, automatically treat B as smaller
return 1;
} else if (dateAUpdatedAt && dateBUpdatedAt) {
// if they both exist and are NOT the same, do comparison
return dateAUpdatedAt - dateBUpdatedAt;
} else {
// if they do not exist, compare created_at instead
const dateA = Date.parse(a.created_at || ''); // Date.parse('') will yield NaN
const dateB = Date.parse(b.created_at || ''); // Date.parse('') will yield NaN
if (isNaN(dateA) || isNaN(dateB)) return 0;
return dateA - dateB;
}
};

export const selectBestVersionsForStudyset = (baseStudies: Array<BaseStudy>): string[] => {
const selectedVersions = baseStudies.map((baseStudy) => {
const sortedVersions = (
baseStudy.versions as Array<
const sortedVersions = [
...((baseStudy.versions || []) as Array<
Pick<
StudyReturn,
| 'id'
Expand All @@ -15,30 +40,10 @@ export const selectBestVersionsForStudyset = (baseStudies: Array<BaseStudy>): st
| 'studysets'
| 'user'
>
>
).sort((a, b) => {
const dateAUpdatedAt = Date.parse(a.updated_at || '');
const dateBUpdatedAt = Date.parse(b.updated_at || '');

if (isNaN(dateAUpdatedAt) && dateBUpdatedAt) {
// if update_at A does not exist, automatically treat A as smaller
return -1;
} else if (isNaN(dateBUpdatedAt) && dateAUpdatedAt) {
// if update_at B does not exist, automatically treat B as smaller
return 1;
} else if (dateAUpdatedAt && dateBUpdatedAt && dateAUpdatedAt !== dateBUpdatedAt) {
// if they both exist and are NOT the same, do comparison
return dateAUpdatedAt - dateBUpdatedAt;
} else {
// if they do not exist, compare created_at instead
const dateA = Date.parse(a.created_at || ''); // Date.parse('') will yield NaN
const dateB = Date.parse(b.created_at || ''); // Date.parse('') will yield NaN
if (isNaN(dateA) || isNaN(dateB)) return 0;
return dateA - dateB;
}
});
>),
].sort(lastUpdatedAtSortFn);

return sortedVersions[0].id as string;
return sortedVersions[sortedVersions.length - 1].id as string;
});

return selectedVersions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ const EditStudyDetails: React.FC = React.memo((props) => {
expandIconColor="secondary.main"
sx={[
EditStudyComponentsStyles.accordion,
{ borderTop: '2px solid', borderColor: 'secondary.main' },
{
borderTop: '2px solid',
borderColor: 'secondary.main',
borderTopLeftRadius: '4px !important',
borderTopRightRadius: '4px !important',
},
]}
accordionSummarySx={EditStudyComponentsStyles.accordionSummary}
TitleElement={
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Button } from '@mui/material';
import { Box } from '@mui/material';
import LoadingButton from 'components/Buttons/LoadingButton/LoadingButton';
import {
AnalysisReturn,
Expand All @@ -15,7 +15,6 @@ import {
} from 'pages/Projects/ProjectPage/ProjectStore';

import { useAuth0 } from '@auth0/auth0-react';
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
import {
useCreateStudy,
useGetStudysetById,
Expand All @@ -29,7 +28,6 @@ import {
useStudyAnalyses,
useStudyHasBeenEdited,
useStudyUser,
useStudyUsername,
useUpdateStudyInDB,
useUpdateStudyIsLoading,
} from 'pages/Studies/StudyStore';
Expand All @@ -46,8 +44,9 @@ import {
import { storeNotesToDBNotes } from 'stores/AnnotationStore.helpers';
import API from 'utils/api';
import { arrayToMetadata } from '../EditStudyMetadata/EditStudyMetadata';
import { hasDuplicateStudyAnalysisNames, hasEmptyStudyPoints } from './EditStudySaveButton.helpers';
import EditStudySwapVersionButton from '../EditStudySwapVersionButton/EditStudySwapVersionButton';
import { hasDuplicateStudyAnalysisNames, hasEmptyStudyPoints } from './EditStudySaveButton.helpers';
import { STUDYSET_QUERY_STRING } from 'hooks/studysets/useGetStudysets';

const EditStudySaveButton: React.FC = React.memo((props) => {
const { user } = useAuth0();
Expand Down Expand Up @@ -193,7 +192,7 @@ const EditStudySaveButton: React.FC = React.memo((props) => {
if (!clonedStudyId) throw new Error('study not cloned correctly');

// 2. update the clone with our latest updates
const x = await updateStudy({
await updateStudy({
studyId: clonedStudyId,
study: {
...getNewScrubbedStudyFromStore(),
Expand All @@ -214,6 +213,7 @@ const EditStudySaveButton: React.FC = React.memo((props) => {
studies: updatedStudies,
},
});
queryClient.invalidateQueries(STUDYSET_QUERY_STRING);

// 4. update the project as this keeps track of completion status of studies
replaceStudyWithNewClonedStudy(storeStudy.id, clonedStudyId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,48 @@
import { Button, ButtonGroup, ListItem, ListItemButton, Menu } from '@mui/material';
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
import { useState } from 'react';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
import { Button, ButtonGroup, ListItem, Menu, Typography } from '@mui/material';
import LoadingButton from 'components/Buttons/LoadingButton/LoadingButton';
import ConfirmationDialog from 'components/Dialogs/ConfirmationDialog/ConfirmationDialog';
import { lastUpdatedAtSortFn } from 'components/Dialogs/MoveToExtractionDialog/MoveToExtractionIngest/helpers/utils';
import { useGetStudysetById, useUpdateStudyset } from 'hooks';
import useGetBaseStudyById from 'hooks/studies/useGetBaseStudyById';
import { StudyReturn } from 'neurostore-typescript-sdk';
import { useSnackbar } from 'notistack';
import {
useProjectExtractionReplaceStudyListStatusId,
useProjectExtractionStudysetId,
useProjectId,
} from 'pages/Projects/ProjectPage/ProjectStore';
import { useStudyBaseStudyId, useStudyId } from 'pages/Studies/StudyStore';
import { setAnalysesInAnnotationAsIncluded } from 'pages/helpers/utils';
import { useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useAnnotationId } from 'stores/AnnotationStore.getters';

const EditStudySwapVersionButton: React.FC = (props) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
const open = Boolean(anchorEl);
const baseStudyId = useStudyBaseStudyId();
const projectId = useProjectId();
const studyId = useStudyId();
const { data: baseStudy } = useGetBaseStudyById(baseStudyId || '');
const { mutateAsync: updateStudyset } = useUpdateStudyset();
const updateStudysetWithNewStudyId = useProjectExtractionReplaceStudyListStatusId();
const studysetId = useProjectExtractionStudysetId();
const { data: studyset } = useGetStudysetById(studysetId, false);
const history = useHistory();
const { enqueueSnackbar } = useSnackbar();

const annotationId = useAnnotationId();

const [isSwapping, setIsSwapping] = useState(false);
const [confirmationDialogState, setConfirmationDialogState] = useState<{
isOpen: boolean;
selectedVersion?: string;
}>({
isOpen: false,
selectedVersion: undefined,
});

const handleButtonPress = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
Expand All @@ -15,51 +52,136 @@ const EditStudySwapVersionButton: React.FC = (props) => {
setAnchorEl(null);
};

const handleSwapStudy = () => {};
const handleCloseConfirmationDialog = (confirm?: boolean) => {
if (confirm) {
handleSwapStudy(confirmationDialogState.selectedVersion);
}
setConfirmationDialogState((prev) => ({
...prev,
isOpen: false,
selectedVersion: undefined,
}));
};

const handleSwapStudy = async (versionToSwapTo?: string) => {
if (!annotationId || !studyId || !studysetId || !versionToSwapTo || !studyset?.studies)
return;
if (versionToSwapTo === studyId) {
handleCloseNavMenu();
return;
}
setIsSwapping(true);
try {
handleCloseNavMenu();
const updatedStudyset = [...(studyset.studies as string[])];

const currentStudyBeingEditedIndex = updatedStudyset.findIndex(
(study) => study === studyId
);
if (currentStudyBeingEditedIndex < 0) throw new Error('study not found in studyset');

updatedStudyset[currentStudyBeingEditedIndex] = versionToSwapTo;
await updateStudyset({
studysetId: studysetId,
studyset: {
studies: updatedStudyset,
},
});

updateStudysetWithNewStudyId(studyId, versionToSwapTo);
await setAnalysesInAnnotationAsIncluded(annotationId, versionToSwapTo);

history.push(`/projects/${projectId}/extraction/studies/${versionToSwapTo}`);

enqueueSnackbar('Updated version', { variant: 'success' });
} catch (e) {
console.error(e);
enqueueSnackbar('There was an error selecting another study version', {
variant: 'error',
});
} finally {
setIsSwapping(false);
}
};

const handleSelectVersion = (versionId: string | undefined) => {
if (!versionId) return;
if (versionId === studyId) {
handleCloseNavMenu();
return;
}
setConfirmationDialogState({
isOpen: true,
selectedVersion: versionId,
});
};

const baseStudyVersions = useMemo(() => {
const baseVersions = (baseStudy?.versions || []) as StudyReturn[];
return baseVersions.sort(lastUpdatedAtSortFn).reverse();
}, [baseStudy?.versions]);

return (
<>
<Button
<LoadingButton
isLoading={isSwapping}
sx={{ width: '280px', height: '36px' }}
variant="contained"
disableElevation
color="secondary"
onClick={handleButtonPress}
startIcon={<SwapHorizIcon />}
>
Switch study version
</Button>
text="Switch study version"
></LoadingButton>
<ConfirmationDialog
dialogTitle="Are you sure you want to switch the study version?"
dialogMessage={
<>
<Typography>
You are switching from version {studyId} to version
{confirmationDialogState.selectedVersion || ''}
</Typography>
<Typography gutterBottom sx={{ color: 'error.main', marginBottom: '1rem' }}>
Warning: switching versions will remove any annotations you have created
for this study.
</Typography>
</>
}
onCloseDialog={handleCloseConfirmationDialog}
isOpen={confirmationDialogState.isOpen}
rejectText="Cancel"
/>
<Menu
open={open}
onClose={handleCloseNavMenu}
anchorEl={anchorEl}
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
transformOrigin={{ vertical: 'bottom', horizontal: 'left' }}
>
<ListItem sx={{ padding: '0 1rem' }}>
<ButtonGroup variant="text">
<Button>
Version 238iERGtug | Owner: Neurosynth | Last Updated: Oct 28 2023
</Button>
<Button endIcon={<OpenInNewIcon />}>View </Button>
</ButtonGroup>
</ListItem>
<ListItem sx={{ padding: '0 1rem' }}>
<ButtonGroup variant="text">
<Button>
Version 238iERGtug | Owner: Neurosynth | Last Updated: Oct 28 2023
</Button>
<Button endIcon={<OpenInNewIcon />}>View </Button>
</ButtonGroup>
</ListItem>
<ListItem sx={{ padding: '0 1rem' }}>
<ButtonGroup variant="text">
<Button>
Version 238iERGtug | Owner: Neurosynth | Last Updated: Oct 28 2023
</Button>
<Button endIcon={<OpenInNewIcon />}>View </Button>
</ButtonGroup>
</ListItem>
{baseStudyVersions.map((baseStudyVersion) => (
<ListItem key={baseStudyVersion.id} sx={{ padding: '0.2rem 1rem' }}>
<ButtonGroup variant="text">
<Button
onClick={() => handleSelectVersion(baseStudyVersion.id)}
color={baseStudyVersion.id === studyId ? 'secondary' : 'primary'}
sx={{ width: '440px' }}
>
Switch to version {baseStudyVersion.id} | Owner:{' '}
{!baseStudyVersion.username
? 'neurosynth'
: baseStudyVersion.username}
</Button>
<Button
color={baseStudyVersion.id === studyId ? 'secondary' : 'primary'}
href={`/base-studies/${baseStudyId}/${studyId}`}
target="_blank"
endIcon={<OpenInNewIcon />}
>
View version
</Button>
</ButtonGroup>
</ListItem>
))}
</Menu>
</>
);
Expand Down
Loading

0 comments on commit 579010b

Please sign in to comment.