Skip to content

Commit 876ee4c

Browse files
committed
Merge remote-tracking branch 'upstream/FEAT-1075-extraction-add-a-new-annotation-column-in-the-study-view' into staging
2 parents e5a7c47 + e181aa2 commit 876ee4c

13 files changed

+205
-261
lines changed

compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.helpers.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,7 @@ export const annotationNotesToHotData = (
119119
};
120120
};
121121

122-
export const createColumnHeader = (
123-
colKey: string,
124-
colType: EPropertyType,
125-
allowRemoveColumn: boolean
126-
) => {
122+
export const createColumnHeader = (colKey: string, colType: EPropertyType, allowRemoveColumn: boolean) => {
127123
const allowRemove = allowRemoveColumn
128124
? `<div style="width: 50px; display: flex; align-items: center; justify-content: center">
129125
${renderToString(
@@ -168,17 +164,15 @@ export const createColumns = (noteKeys: NoteKeyType[], disable?: boolean) =>
168164
x.type === EPropertyType.NUMBER
169165
? numericValidator
170166
: x.type === EPropertyType.BOOLEAN
171-
? booleanValidator
172-
: undefined,
167+
? booleanValidator
168+
: undefined,
173169
} as ColumnSettings;
174170
}),
175171
] as ColumnSettings[];
176172

177173
// we can assume that the hashmap maintains order and is sorted by key
178174
// this function gets all merge cells and only merge cells. If a cell does not need to be merged, a mergeCellObj is not creatd
179-
export const getMergeCells = (
180-
hotDataToStudyMapping: Map<number, { studyId: string; analysisId: string }>
181-
) => {
175+
export const getMergeCells = (hotDataToStudyMapping: Map<number, { studyId: string; analysisId: string }>) => {
182176
const mergeCells: MergeCellsSettings[] = [];
183177

184178
let studyId: string;

compose/neurosynth-frontend/src/pages/Annotations/hooks/useEditAnnotationsHotTable.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ const useEditAnnotationsHotTable = (annotationId?: string, disableEdit?: boolean
4949
annotationNote.study_name && annotationNote.study_year
5050
? `(${annotationNote.study_year}) ${annotationNote.study_name}`
5151
: annotationNote.study_name
52-
? annotationNote.study_name
53-
: '';
52+
? annotationNote.study_name
53+
: '';
5454

5555
const analysisName = annotationNote.analysis_name || '';
5656

compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnalysis.tsx

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,20 @@
1-
import { Box, Button, Typography } from '@mui/material';
2-
import ConfirmationDialog from 'components/Dialogs/ConfirmationDialog';
3-
import { useDeleteAnalysis } from 'pages/Study/store/StudyStore';
4-
import { useState } from 'react';
5-
import { useDeleteAnnotationNote } from 'stores/AnnotationStore.actions';
1+
import { Box, Typography } from '@mui/material';
2+
import EditStudyAnalysisDetails from 'pages/Study/components/EditStudyAnalysisDetails';
63
import EditStudyAnalysisPoints from 'pages/Study/components/EditStudyAnalysisPoints';
74
import StudyAnalysisWarnings from 'pages/Study/components/StudyAnalysisWarnings';
8-
import EditStudyAnalysisDetails from 'pages/Study/components/EditStudyAnalysisDetails';
95

106
const EditStudyAnalysis: React.FC<{
117
analysisId?: string;
128
onDeleteAnalysis: () => void;
139
}> = (props) => {
14-
const deleteAnalysis = useDeleteAnalysis();
15-
const deleteAnnotationNote = useDeleteAnnotationNote();
16-
17-
const [dialogIsOpen, setDialogIsOpen] = useState(false);
18-
1910
if (!props.analysisId) {
2011
return <Typography sx={{ color: 'warning.dark' }}>No analysis selected</Typography>;
2112
}
2213

23-
const handleCloseDialog = (confirm?: boolean) => {
24-
if (confirm && props.analysisId) {
25-
deleteAnalysis(props.analysisId);
26-
deleteAnnotationNote(props.analysisId);
27-
props.onDeleteAnalysis();
28-
}
29-
setDialogIsOpen(false);
30-
};
31-
3214
return (
3315
<Box sx={{ marginBottom: '2rem' }}>
3416
<StudyAnalysisWarnings analysisId={props.analysisId} />
35-
<EditStudyAnalysisDetails analysisId={props.analysisId} />
17+
<EditStudyAnalysisDetails analysisId={props.analysisId} onDeleteAnalysis={props.onDeleteAnalysis} />
3618
<EditStudyAnalysisPoints analysisId={props.analysisId} />
3719
{/* TODO: This can be added back later when we have a better understanding of where it fits in as currently, all meta-analysis algorithms do not use this */}
3820
{/* <Box sx={{ marginTop: '2rem' }}>
@@ -41,18 +23,6 @@ const EditStudyAnalysis: React.FC<{
4123
</Typography>
4224
<EditAnalysisConditions analysisId={props.analysisId} />
4325
</Box> */}
44-
<Box sx={{ marginTop: '2rem' }}>
45-
<ConfirmationDialog
46-
isOpen={dialogIsOpen}
47-
dialogTitle="Are you sure you want to delete this analysis?"
48-
onCloseDialog={handleCloseDialog}
49-
confirmText="delete analysis"
50-
rejectText="cancel"
51-
/>
52-
<Button variant="contained" onClick={() => setDialogIsOpen(true)} disableElevation color="error">
53-
Delete Analysis
54-
</Button>
55-
</Box>
5626
</Box>
5727
);
5828
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Button, ButtonOwnProps } from '@mui/material';
2+
import ConfirmationDialog from 'components/Dialogs/ConfirmationDialog';
3+
import { useDeleteAnalysis } from 'pages/Study/store/StudyStore';
4+
import { useState } from 'react';
5+
import { useDeleteAnnotationNote } from 'stores/AnnotationStore.actions';
6+
7+
const EditStudyAnalysisDeleteButton: React.FC<
8+
ButtonOwnProps & {
9+
analysisId?: string;
10+
onDeleteAnalysis: () => void;
11+
}
12+
> = ({ analysisId, onDeleteAnalysis, ...buttonProps }) => {
13+
const deleteAnalysis = useDeleteAnalysis();
14+
const deleteAnnotationNote = useDeleteAnnotationNote();
15+
16+
const [dialogIsOpen, setDialogIsOpen] = useState(false);
17+
18+
const handleCloseDialog = (confirm?: boolean) => {
19+
if (confirm && analysisId) {
20+
deleteAnalysis(analysisId);
21+
deleteAnnotationNote(analysisId);
22+
onDeleteAnalysis();
23+
}
24+
setDialogIsOpen(false);
25+
};
26+
27+
return (
28+
<>
29+
<ConfirmationDialog
30+
isOpen={dialogIsOpen}
31+
dialogTitle="Are you sure you want to delete this analysis?"
32+
onCloseDialog={handleCloseDialog}
33+
confirmText="delete analysis"
34+
rejectText="cancel"
35+
/>
36+
<Button {...buttonProps} onClick={() => setDialogIsOpen(true)}>
37+
{buttonProps.children}
38+
</Button>
39+
</>
40+
);
41+
};
42+
43+
export default EditStudyAnalysisDeleteButton;

compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnalysisDetails.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@ import {
77
import { IStoreAnalysis } from 'pages/Study/store/StudyStore.helpers';
88
import { useEffect } from 'react';
99
import { useUpdateAnnotationNoteName } from 'stores/AnnotationStore.actions';
10+
import EditStudyAnalysisDeleteButton from './EditStudyAnalysisDeleteButton';
1011

11-
const EditStudyAnalysisDetails: React.FC<{ analysisId?: string }> = (props) => {
12+
const EditStudyAnalysisDetails: React.FC<{ analysisId?: string; onDeleteAnalysis: () => void }> = (props) => {
1213
const addOrUpdateAnalysis = useAddOrUpdateAnalysis();
1314
const name = useStudyAnalysisName(props.analysisId);
1415
const description = useStudyAnalysisDescription(props.analysisId);
1516
const updateAnnotationNoteName = useUpdateAnnotationNoteName();
1617

1718
useEffect(() => {
1819
if (!props.analysisId) return;
19-
let debounce: NodeJS.Timeout;
20-
debounce = setTimeout(() => {
20+
const debounce: NodeJS.Timeout = setTimeout(() => {
2121
updateAnnotationNoteName({
2222
analysis: props.analysisId,
2323
analysis_name: name,
@@ -29,11 +29,7 @@ const EditStudyAnalysisDetails: React.FC<{ analysisId?: string }> = (props) => {
2929
};
3030
}, [name, props.analysisId, updateAnnotationNoteName]);
3131

32-
const handleUpdateAnalysisDetails = (
33-
field: keyof IStoreAnalysis,
34-
analysisId: string,
35-
value: string
36-
) => {
32+
const handleUpdateAnalysisDetails = (field: keyof IStoreAnalysis, analysisId: string, value: string) => {
3733
if (!analysisId) return;
3834
addOrUpdateAnalysis({
3935
id: analysisId,
@@ -43,9 +39,19 @@ const EditStudyAnalysisDetails: React.FC<{ analysisId?: string }> = (props) => {
4339

4440
return (
4541
<Box sx={{ width: '100%' }}>
46-
<Typography sx={{ marginBottom: '1rem', fontWeight: 'bold' }}>
47-
Analysis Details
48-
</Typography>
42+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
43+
<Typography sx={{ marginBottom: '1rem', fontWeight: 'bold' }}>Analysis Details</Typography>
44+
<EditStudyAnalysisDeleteButton
45+
variant="contained"
46+
disableElevation
47+
color="error"
48+
size="small"
49+
onDeleteAnalysis={props.onDeleteAnalysis}
50+
analysisId={props.analysisId}
51+
>
52+
Delete Analysis
53+
</EditStudyAnalysisDeleteButton>
54+
</Box>
4955
<TextField
5056
label="name"
5157
size="small"
@@ -57,11 +63,7 @@ const EditStudyAnalysisDetails: React.FC<{ analysisId?: string }> = (props) => {
5763
/>
5864
<TextField
5965
onChange={(event) => {
60-
handleUpdateAnalysisDetails(
61-
'description',
62-
props.analysisId || '',
63-
event.target.value
64-
);
66+
handleUpdateAnalysisDetails('description', props.analysisId || '', event.target.value);
6567
}}
6668
label="description"
6769
size="small"

compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.helpers.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,6 @@ export const HotSettings: HotTableProps = {
2727
},
2828
};
2929

30-
export const createStudyAnnotationColHeaders = (noteKeys: NoteKeyType[]): string[] => {
31-
return [
32-
'Analysis Name',
33-
'Analysis Description',
34-
...noteKeys.map((x) => `<div class="${styles[x.type]}">${x.key}</div>`),
35-
];
36-
};
37-
3830
export const createStudyAnnotationColumns = (noteKeys: NoteKeyType[], readonly: boolean) =>
3931
[
4032
{

compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.tsx

Lines changed: 75 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
11
import { HotTable } from '@handsontable/react';
22
import { Box } from '@mui/material';
3-
import { CellChange, ChangeSource, RangeType } from 'handsontable/common';
4-
import { useMemo, useRef } from 'react';
5-
import { useAnnotationNoteKeys, useUpdateAnnotationNotes } from 'stores/AnnotationStore.actions';
3+
import AddMetadataRow from 'components/EditMetadata/AddMetadataRow';
4+
import { getType, IMetadataRowModel } from 'components/EditMetadata/EditMetadata.types';
65
import { sanitizePaste } from 'components/HotTables/HotTables.utils';
7-
import useEditStudyAnnotationsHotTable from 'pages/Study/components/useEditStudyAnnotationsHotTable';
6+
import { CellChange } from 'handsontable/common';
7+
import { useUserCanEdit } from 'hooks';
8+
import { useProjectUser } from 'pages/Project/store/ProjectStore';
89
import { HotSettings } from 'pages/Study/components/EditStudyAnnotationsHotTable.helpers';
10+
import useEditStudyAnnotationsHotTable from 'pages/Study/hooks/useEditStudyAnnotationsHotTable';
11+
import { useMemo, useRef } from 'react';
12+
import {
13+
useAnnotationNoteKeys,
14+
useCreateAnnotationColumn,
15+
useUpdateAnnotationNotes,
16+
} from 'stores/AnnotationStore.actions';
917

1018
const EditStudyAnnotationsHotTable: React.FC<{ readonly?: boolean }> = ({ readonly = false }) => {
1119
const hotTableRef = useRef<HotTable>(null);
1220
const noteKeys = useAnnotationNoteKeys();
1321
const updateNotes = useUpdateAnnotationNotes();
14-
const { colWidths, colHeaders, columns, hiddenRows, data } = useEditStudyAnnotationsHotTable(hotTableRef, readonly);
22+
const createAnnotationColumn = useCreateAnnotationColumn();
23+
const { colWidths, colHeaders, columns, hiddenRows, data } = useEditStudyAnnotationsHotTable(readonly);
24+
const projectUser = useProjectUser();
25+
const canEdit = useUserCanEdit(projectUser || undefined);
1526

16-
const handleAfterChange = (changes: CellChange[] | null, source: ChangeSource) => {
27+
const handleAfterChange = (changes: CellChange[] | null) => {
1728
if (!data || !noteKeys || !changes) return;
1829

1930
const updatedNotes = [...data];
@@ -37,35 +48,74 @@ const EditStudyAnnotationsHotTable: React.FC<{ readonly?: boolean }> = ({ readon
3748
updateNotes(updatedNotes);
3849
};
3950

40-
const handleBeforePaste = (pastedData: any[][], coords: RangeType[]) => {
51+
const handleBeforePaste = (pastedData: unknown[][]) => {
4152
if (!data) return false;
4253
sanitizePaste(pastedData);
4354
return true;
4455
};
4556

57+
const handleAddHotColumn = (row: IMetadataRowModel) => {
58+
if (!noteKeys) return false;
59+
const trimmedKey = row.metadataKey.trim();
60+
if (noteKeys.find((x) => x.key === trimmedKey)) return false;
61+
62+
createAnnotationColumn({
63+
key: trimmedKey,
64+
type: getType(row.metadataValue),
65+
});
66+
67+
return true;
68+
};
69+
4670
const memoizedData = useMemo(() => {
4771
return JSON.parse(JSON.stringify(data || []));
4872
}, [data]);
4973

5074
return (
51-
<Box sx={{ width: '100%', height: '100%' }}>
52-
<HotTable
53-
{...HotSettings}
54-
afterChange={handleAfterChange}
55-
beforePaste={handleBeforePaste}
56-
width="100%"
57-
height="auto"
58-
stretchH="all"
59-
hiddenRows={{
60-
rows: hiddenRows,
61-
indicators: false,
62-
}}
63-
colWidths={colWidths}
64-
columns={columns}
65-
colHeaders={colHeaders}
66-
data={memoizedData}
67-
ref={hotTableRef}
68-
/>
75+
<Box>
76+
{canEdit && !readonly && (
77+
<Box
78+
sx={{
79+
mb: 3,
80+
width: {
81+
xs: '100%',
82+
md: '80%',
83+
lg: '70%',
84+
},
85+
display: 'grid',
86+
gridTemplateColumns: 'auto 1fr auto',
87+
gap: 4,
88+
}}
89+
>
90+
<AddMetadataRow
91+
keyPlaceholderText="New Column"
92+
onAddMetadataRow={handleAddHotColumn}
93+
showMetadataValueInput={false}
94+
allowNumber={false}
95+
allowNone={false}
96+
errorMessage="can't add column (key already exists)"
97+
/>
98+
</Box>
99+
)}
100+
<Box sx={{ width: '100%', height: '100%' }}>
101+
<HotTable
102+
{...HotSettings}
103+
afterChange={handleAfterChange}
104+
beforePaste={handleBeforePaste}
105+
width="100%"
106+
height="auto"
107+
stretchH="all"
108+
hiddenRows={{
109+
rows: hiddenRows,
110+
indicators: false,
111+
}}
112+
colWidths={colWidths}
113+
columns={columns}
114+
colHeaders={colHeaders}
115+
data={memoizedData}
116+
ref={hotTableRef}
117+
/>
118+
</Box>
69119
</Box>
70120
);
71121
};

0 commit comments

Comments
 (0)