Skip to content

Commit 1805f25

Browse files
nicoaleejdkent
andauthored
654 ux adding a new analysis puts it at top (#703)
* feat: update with latest openapi spec and fix up create-analysis-ux * chore: remove unused code * fix: align display analyses list component * update submodule --------- Co-authored-by: James Kent <[email protected]>
1 parent 8d2e619 commit 1805f25

File tree

12 files changed

+372
-98
lines changed

12 files changed

+372
-98
lines changed

compose/neurosynth-frontend/src/components/DisplayStudy/DisplayAnalyses/DisplayAnalyses.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,45 @@
11
import { Box } from '@mui/material';
22
import DisplayAnalysesList from './DisplayAnalysesList/DisplayAnalysesList';
3-
import { useEffect, useState } from 'react';
3+
import { useEffect, useMemo, useState } from 'react';
44
import DisplayAnalysis from './DisplayAnalysis/DisplayAnalysis';
55
import { IStoreAnalysis } from 'pages/Studies/StudyStore.helpers';
66

77
const DisplayAnalyses: React.FC<{
88
id: string | undefined;
99
analyses: IStoreAnalysis[];
1010
}> = (props) => {
11-
const [selectedAnalysisIndex, setSelectedAnalysisIndex] = useState(0);
11+
const [selectedAnalysisId, setSelectedAnalysisId] = useState<string | undefined>('');
1212

1313
useEffect(() => {
14-
setSelectedAnalysisIndex(0);
15-
}, [props.id]);
14+
if (props.analyses.length <= 0) return;
15+
setSelectedAnalysisId(props.analyses[0].id);
16+
}, [props.analyses, props.id]);
1617

17-
const handleSelectAnalysis = (index: number) => {
18-
if (props.analyses[index]) {
19-
setSelectedAnalysisIndex(index);
18+
const handleSelectAnalysis = (id: string) => {
19+
const index = props.analyses.findIndex((a) => a.id === id);
20+
if (index < 0) {
21+
return;
2022
} else {
21-
setSelectedAnalysisIndex(0);
23+
setSelectedAnalysisId(props.analyses[index].id);
2224
}
2325
};
2426

25-
const selectedAnalysis = props.analyses[selectedAnalysisIndex];
27+
const selectedAnalysis = useMemo(() => {
28+
return props.analyses.find((a) => a.id === selectedAnalysisId);
29+
}, [props.analyses, selectedAnalysisId]);
2630

2731
return (
2832
<Box sx={{ display: 'flex' }}>
2933
<DisplayAnalysesList
30-
selectedIndex={selectedAnalysisIndex}
34+
selectedId={selectedAnalysisId}
3135
onSelectAnalysisIndex={handleSelectAnalysis}
3236
analyses={props.analyses}
3337
/>
34-
<Box sx={{ padding: '1rem', width: 'calc(100% - 250px - 2rem)', height: '100%' }}>
35-
<DisplayAnalysis {...selectedAnalysis} />
36-
</Box>
38+
{selectedAnalysis && (
39+
<Box sx={{ padding: '1rem', width: 'calc(100% - 250px - 2rem)', height: '100%' }}>
40+
<DisplayAnalysis {...selectedAnalysis} />
41+
</Box>
42+
)}
3743
</Box>
3844
);
3945
};

compose/neurosynth-frontend/src/components/DisplayStudy/DisplayAnalyses/DisplayAnalysesList/DisplayAnalysesList.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { IStoreAnalysis } from 'pages/Studies/StudyStore.helpers';
44

55
const DisplayAnalysesList: React.FC<{
66
analyses: IStoreAnalysis[];
7-
selectedIndex: number;
8-
onSelectAnalysisIndex: (index: number) => void;
7+
selectedId: string | undefined;
8+
onSelectAnalysisIndex: (id: string) => void;
99
}> = (props) => {
1010
return (
1111
<Box
@@ -23,13 +23,12 @@ const DisplayAnalysesList: React.FC<{
2323
}}
2424
disablePadding
2525
>
26-
{props.analyses.map((analysis, index) => (
26+
{props.analyses.map((analysis) => (
2727
<EditAnalysesListItem
28-
key={analysis.id || index}
28+
key={analysis.id}
2929
analysis={analysis}
30-
index={index}
31-
selected={props.selectedIndex === index}
32-
onSelectAnalysis={(id, i) => props.onSelectAnalysisIndex(i)}
30+
selected={(props.selectedId || undefined) === (analysis.id || null)}
31+
onSelectAnalysis={(id) => props.onSelectAnalysisIndex(id)}
3332
/>
3433
))}
3534
</List>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const useDisplayWarnings = jest.fn().mockReturnValue({
2+
hasDuplicateName: false,
3+
hasNoName: false,
4+
hasNoPoints: false,
5+
hasNonMNICoordinates: false,
6+
});
7+
8+
export default useDisplayWarnings;

compose/neurosynth-frontend/src/components/EditStudyComponents/EditAnalyses/EditAnalyses.tsx

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,60 @@
11
import { Add } from '@mui/icons-material';
22
import { Box, Button, Divider, Typography } from '@mui/material';
3-
import CreateDetailsDialog from 'components/Dialogs/CreateDetailsDialog/CreateDetailsDialog';
4-
import { useAddOrUpdateAnalysis, useNumStudyAnalyses, useStudyId } from 'pages/Studies/StudyStore';
5-
import { useCallback, useState } from 'react';
6-
import EditAnalysesList from './EditAnalysesList/EditAnalysesList';
7-
import EditAnalysis from './EditAnalysis/EditAnalysis';
8-
import NeurosynthAccordion from 'components/NeurosynthAccordion/NeurosynthAccordion';
93
import EditStudyComponentsStyles from 'components/EditStudyComponents/EditStudyComponents.styles';
4+
import NeurosynthAccordion from 'components/NeurosynthAccordion/NeurosynthAccordion';
5+
import { useAddOrUpdateAnalysis, useStudyAnalyses, useStudyId } from 'pages/Studies/StudyStore';
6+
import React, { useCallback, useEffect, useState } from 'react';
107
import { useCreateAnnotationNote } from 'stores/AnnotationStore.actions';
11-
import React from 'react';
8+
import EditAnalysesList from './EditAnalysesList/EditAnalysesList';
9+
import EditAnalysis from './EditAnalysis/EditAnalysis';
1210

1311
const EditAnalyses: React.FC = React.memo((props) => {
14-
const numAnalyses = useNumStudyAnalyses();
12+
const analyses = useStudyAnalyses();
1513
const studyId = useStudyId();
1614
const addOrUpdateAnalysis = useAddOrUpdateAnalysis();
1715
const createAnnotationNote = useCreateAnnotationNote();
1816
const [selectedAnalysisId, setSelectedAnalysisId] = useState<string>();
19-
const [createNewAnalysisDialogIsOpen, setCreateNewAnalysisDialogIsOpen] = useState(false);
2017

21-
const handleCreateNewAnalysis = (name: string, description: string) => {
18+
const handleCreateNewAnalysis = () => {
2219
if (!studyId) return;
2320

2421
const createdAnalysis = addOrUpdateAnalysis({
25-
name,
26-
description,
22+
name: '',
23+
description: '',
2724
isNew: true,
2825
conditions: [],
2926
});
3027

3128
if (!createdAnalysis.id) return;
3229

33-
createAnnotationNote(createdAnalysis.id, studyId, name);
30+
createAnnotationNote(createdAnalysis.id, studyId, '');
3431
};
3532

3633
const handleSelectAnalysis = useCallback((analysisId: string) => {
3734
setSelectedAnalysisId(analysisId);
3835
}, []);
3936

40-
const handleOnDeleteAnalysis = () => {
41-
setSelectedAnalysisId(undefined);
37+
const handleAfterAnalysisDeleted = () => {
38+
if (analyses.length <= 1) {
39+
setSelectedAnalysisId(undefined);
40+
return;
41+
}
42+
const analysisIndex = analyses.findIndex((analysis) => analysis.id === selectedAnalysisId);
43+
44+
if (analysisIndex === 0) {
45+
setSelectedAnalysisId(analyses[1].id);
46+
return;
47+
}
48+
setSelectedAnalysisId(analyses[analysisIndex - 1].id);
4249
};
4350

51+
useEffect(() => {
52+
if (!selectedAnalysisId) {
53+
// select the first analysis on first render
54+
setSelectedAnalysisId(analyses[0].id);
55+
}
56+
}, [analyses, selectedAnalysisId]);
57+
4458
return (
4559
<NeurosynthAccordion
4660
elevation={0}
@@ -60,19 +74,13 @@ const EditAnalyses: React.FC = React.memo((props) => {
6074
>
6175
<Box sx={{ width: '100%', margin: '0.5rem 0' }}>
6276
<Box sx={{ marginBottom: '1rem' }}>
63-
{numAnalyses === 0 && (
77+
{analyses.length === 0 && (
6478
<Typography sx={{ color: 'warning.dark' }} gutterBottom>
6579
There are no analyses for this study.
6680
</Typography>
6781
)}
68-
<CreateDetailsDialog
69-
titleText="Create new analysis"
70-
onCreate={handleCreateNewAnalysis}
71-
onCloseDialog={() => setCreateNewAnalysisDialogIsOpen(false)}
72-
isOpen={createNewAnalysisDialogIsOpen}
73-
/>
7482
<Button
75-
onClick={() => setCreateNewAnalysisDialogIsOpen(true)}
83+
onClick={handleCreateNewAnalysis}
7684
sx={{ width: '150px' }}
7785
variant="contained"
7886
disableElevation
@@ -81,11 +89,12 @@ const EditAnalyses: React.FC = React.memo((props) => {
8189
analysis
8290
</Button>
8391
</Box>
84-
{numAnalyses > 0 && (
92+
{analyses.length > 0 && (
8593
<>
8694
<Divider />
8795
<Box sx={{ display: 'flex' }}>
8896
<EditAnalysesList
97+
analyses={analyses}
8998
selectedAnalysisId={selectedAnalysisId}
9099
onSelectAnalysis={handleSelectAnalysis}
91100
/>
@@ -96,7 +105,7 @@ const EditAnalyses: React.FC = React.memo((props) => {
96105
}}
97106
>
98107
<EditAnalysis
99-
onDeleteAnalysis={handleOnDeleteAnalysis}
108+
onDeleteAnalysis={handleAfterAnalysisDeleted}
100109
analysisId={selectedAnalysisId}
101110
/>
102111
</Box>
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { render, screen } from '@testing-library/react';
2+
import EditAnalysesList from './EditAnalysesList';
3+
import { IStoreAnalysis } from 'pages/Studies/StudyStore.helpers';
4+
import userEvent from '@testing-library/user-event';
5+
6+
jest.mock('components/EditStudyComponents/EditAnalyses/EditAnalysesList/EditAnalysesListItem.tsx');
7+
8+
describe('EditAnalysesList Component', () => {
9+
it('should render', () => {
10+
render(
11+
<EditAnalysesList onSelectAnalysis={() => {}} selectedAnalysisId="" analyses={[]} />
12+
);
13+
});
14+
15+
it('should show two analyses in the list', () => {
16+
const mockAnalyses: IStoreAnalysis[] = [
17+
{
18+
id: 'test-id-1',
19+
name: 'test-name-1',
20+
description: 'test-description-1',
21+
isNew: false,
22+
conditions: [],
23+
points: [],
24+
pointSpace: {
25+
value: '',
26+
label: '',
27+
},
28+
pointStatistic: {
29+
value: '',
30+
label: '',
31+
},
32+
},
33+
{
34+
id: 'test-id-2',
35+
name: 'test-name-2',
36+
description: 'test-description-2',
37+
isNew: false,
38+
conditions: [],
39+
points: [],
40+
pointSpace: {
41+
value: '',
42+
label: '',
43+
},
44+
pointStatistic: {
45+
value: '',
46+
label: '',
47+
},
48+
},
49+
];
50+
render(
51+
<EditAnalysesList
52+
onSelectAnalysis={() => {}}
53+
selectedAnalysisId=""
54+
analyses={mockAnalyses}
55+
/>
56+
);
57+
58+
mockAnalyses.forEach((mockAnalysis) => {
59+
expect(screen.getByText(mockAnalysis.name as string)).toBeInTheDocument();
60+
expect(screen.getByText(mockAnalysis.description as string)).toBeInTheDocument();
61+
});
62+
});
63+
64+
it('should call the onSelectAnalysis function when analysis is selected', () => {
65+
const handleOnSelectAnalysisMock = jest.fn();
66+
const mockAnalyses: IStoreAnalysis[] = [
67+
{
68+
id: 'test-id-1',
69+
name: 'test-name-1',
70+
description: 'test-description-1',
71+
isNew: false,
72+
conditions: [],
73+
points: [],
74+
pointSpace: {
75+
value: '',
76+
label: '',
77+
},
78+
pointStatistic: {
79+
value: '',
80+
label: '',
81+
},
82+
},
83+
];
84+
render(
85+
<EditAnalysesList
86+
onSelectAnalysis={handleOnSelectAnalysisMock}
87+
selectedAnalysisId=""
88+
analyses={mockAnalyses}
89+
/>
90+
);
91+
92+
userEvent.click(screen.getByTestId('test-trigger-select-analysis'));
93+
expect(handleOnSelectAnalysisMock).toHaveBeenCalledWith(mockAnalyses[0].id);
94+
});
95+
});

compose/neurosynth-frontend/src/components/EditStudyComponents/EditAnalyses/EditAnalysesList/EditAnalysesList.tsx

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,22 @@
11
import { Box, List } from '@mui/material';
2-
import { useStudyAnalyses } from 'pages/Studies/StudyStore';
3-
import { useCallback, useEffect, useState } from 'react';
2+
import { IStoreAnalysis } from 'pages/Studies/StudyStore.helpers';
3+
import { useCallback } from 'react';
44
import EditAnalysesListItem from './EditAnalysesListItem';
55

66
const EditAnalysesList: React.FC<{
77
onSelectAnalysis: (analysisId: string) => void;
88
selectedAnalysisId?: string;
9+
analyses: IStoreAnalysis[];
910
}> = (props) => {
10-
const { onSelectAnalysis, selectedAnalysisId } = props;
11-
12-
const analyses = useStudyAnalyses();
13-
const [selectedIndex, setSelectedIndex] = useState(0);
11+
const { onSelectAnalysis, selectedAnalysisId, analyses } = props;
1412

1513
const handleSelectAnalysis = useCallback(
16-
(analysisId: string, index: number) => {
17-
setSelectedIndex(index);
14+
(analysisId: string) => {
1815
onSelectAnalysis(analysisId);
1916
},
2017
[onSelectAnalysis]
2118
);
2219

23-
useEffect(() => {
24-
if (!analyses[0]?.id) return;
25-
26-
if (!selectedAnalysisId) {
27-
// select the first analysis on first render
28-
onSelectAnalysis(analyses[0].id);
29-
return;
30-
}
31-
32-
if (!analyses.find((x) => x.id === selectedAnalysisId)) {
33-
// when a new analysis is created and saved in the DB, it is given a neurostore ID which replaces the temporary one
34-
// initially given. We need to handle this case, otherwise the UI will show nothing is currently selected
35-
const newAnalysisId = analyses[selectedIndex].id;
36-
if (!newAnalysisId) return;
37-
onSelectAnalysis(newAnalysisId);
38-
}
39-
}, [analyses, onSelectAnalysis, selectedIndex, selectedAnalysisId]);
40-
4120
return (
4221
<Box
4322
sx={{
@@ -54,13 +33,12 @@ const EditAnalysesList: React.FC<{
5433
}}
5534
disablePadding
5635
>
57-
{analyses.map((analysis, index) => (
36+
{analyses.map((analysis) => (
5837
<EditAnalysesListItem
59-
key={analysis.id || index}
38+
key={analysis.id}
6039
analysis={analysis}
61-
index={index}
6240
onSelectAnalysis={handleSelectAnalysis}
63-
selected={(analysis.id || null) === (props.selectedAnalysisId || undefined)}
41+
selected={(analysis.id || null) === (selectedAnalysisId || undefined)}
6442
/>
6543
))}
6644
</List>

0 commit comments

Comments
 (0)