diff --git a/compose/neurosynth-frontend/src/components/Dialogs/CreateMetaAnalysisSpecificationDialog/CreateMetaAnalysisSpecificationSelectionStep/SelectAnalysesComponent/SelectAnalysesComponent.tsx b/compose/neurosynth-frontend/src/components/Dialogs/CreateMetaAnalysisSpecificationDialog/CreateMetaAnalysisSpecificationSelectionStep/SelectAnalysesComponent/SelectAnalysesComponent.tsx index 03a745d4c..326eee2a6 100644 --- a/compose/neurosynth-frontend/src/components/Dialogs/CreateMetaAnalysisSpecificationDialog/CreateMetaAnalysisSpecificationSelectionStep/SelectAnalysesComponent/SelectAnalysesComponent.tsx +++ b/compose/neurosynth-frontend/src/components/Dialogs/CreateMetaAnalysisSpecificationDialog/CreateMetaAnalysisSpecificationSelectionStep/SelectAnalysesComponent/SelectAnalysesComponent.tsx @@ -17,7 +17,7 @@ import NeurosynthTableStyles from 'components/Tables/NeurosynthTable/NeurosynthT import { EPropertyType } from 'components/EditMetadata'; import { useGetAnnotationById } from 'hooks'; import { NoteCollectionReturn } from 'neurostore-typescript-sdk'; -import { AnnotationNoteValue } from 'components/HotTables/helpers/utils'; +import { AnnotationNoteValue } from 'components/HotTables/HotTables.types'; export const getFilteredAnnotationNotes = ( annotationNotes: NoteCollectionReturn[], diff --git a/compose/neurosynth-frontend/src/components/DisplayStudy/DisplayAnalyses/DisplayAnalysis/DisplayPoints/DisplayPoints.tsx b/compose/neurosynth-frontend/src/components/DisplayStudy/DisplayAnalyses/DisplayAnalysis/DisplayPoints/DisplayPoints.tsx index 98fe39272..2ae996c25 100644 --- a/compose/neurosynth-frontend/src/components/DisplayStudy/DisplayAnalyses/DisplayAnalysis/DisplayPoints/DisplayPoints.tsx +++ b/compose/neurosynth-frontend/src/components/DisplayStudy/DisplayAnalyses/DisplayAnalysis/DisplayPoints/DisplayPoints.tsx @@ -1,7 +1,7 @@ import { HotTable } from '@handsontable/react'; import { Box, Typography } from '@mui/material'; import { registerAllModules } from 'handsontable/registry'; -import styles from 'components/HotTables/AnnotationsHotTable/AnnotationsHotTable.module.css'; +import styles from 'components/HotTables/HotTables.module.css'; import { IStorePoint, MapOrSpaceType } from 'pages/Studies/StudyStore.helpers'; import { useEffect, useRef } from 'react'; diff --git a/compose/neurosynth-frontend/src/components/EditAnnotations/EditAnnotations.tsx b/compose/neurosynth-frontend/src/components/EditAnnotations/EditAnnotations.tsx index 72e167e55..251aa9b69 100644 --- a/compose/neurosynth-frontend/src/components/EditAnnotations/EditAnnotations.tsx +++ b/compose/neurosynth-frontend/src/components/EditAnnotations/EditAnnotations.tsx @@ -1,169 +1,99 @@ import LoadingButton from 'components/Buttons/LoadingButton/LoadingButton'; +import EditAnnotationsHotTable from 'components/HotTables/EditAnnotationsHotTable/EditAnnotationsHotTable'; import StateHandlerComponent from 'components/StateHandlerComponent/StateHandlerComponent'; -import { DetailedSettings as MergeCellsSettings } from 'handsontable/plugins/mergeCells'; -import { ColumnSettings } from 'handsontable/settings'; -import { useGetAnnotationById, useUpdateAnnotationById } from 'hooks'; -import { NoteCollectionReturn } from 'neurostore-typescript-sdk'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import AnnotationsHotTable from 'components/HotTables/AnnotationsHotTable/AnnotationsHotTable'; -import { - AnnotationNoteValue, - NoteKeyType, - annotationNotesToHotData, - createColumns, - getMergeCells, - hotDataToAnnotationNotes, - noteKeyArrToObj, - noteKeyObjToArr, -} from '../HotTables/helpers/utils'; -import { useAuth0 } from '@auth0/auth0-react'; +import { useUpdateAnnotationById } from 'hooks'; import { useSnackbar } from 'notistack'; +import { useCallback, useState } from 'react'; const hardCodedColumns = ['Study', 'Analysis']; const EditAnnotations: React.FC<{ annotationId: string }> = (props) => { - const { user } = useAuth0(); const { enqueueSnackbar } = useSnackbar(); const { mutate, isLoading: updateAnnotationIsLoading } = useUpdateAnnotationById( props.annotationId ); - const { - data, - isLoading: getAnnotationIsLoading, - isError, - } = useGetAnnotationById(props.annotationId); + // const { + // theUserOwnsThisAnnotation, + // hotDataToStudyMapping, + // noteKeys, + // hotData, + // hotColumns, + // mergeCells, + // getAnnotationIsError, + // getAnnotationIsLoading, + // } = useEditAnnotations(); + // const [annotationIsEdited, setAnnotationIsEdited] = useState(false); - const theLoggedInUserOwnsThisAnnotation = (user?.sub || null) === (data?.user || undefined); + // const handleClickSave = () => { + // if (!props.annotationId) return; + // if (!theUserOwnsThisAnnotation) { + // enqueueSnackbar('You do not have permission to edit this annotation', { + // variant: 'error', + // }); + // return; + // } - // tracks the changes made to hot table - const hotTableDataUpdatesRef = useRef<{ - initialized: boolean; - hotData: (string | number | boolean | null)[][]; - noteKeys: NoteKeyType[]; - }>({ - initialized: false, - hotData: [], - noteKeys: [], - }); - const [annotationIsEdited, setAnnotationIsEdited] = useState(false); - const [initialAnnotationHotState, setInitialAnnotationHotState] = useState<{ - hotDataToStudyMapping: Map; - noteKeys: NoteKeyType[]; - hotData: AnnotationNoteValue[][]; - hotColumns: ColumnSettings[]; - mergeCells: MergeCellsSettings[]; - }>({ - hotDataToStudyMapping: new Map(), - noteKeys: [], - hotData: [], - hotColumns: [], - mergeCells: [], - }); + // const updatedAnnotationNotes = hotDataToAnnotationNotes( + // hotData, + // hotDataToStudyMapping, + // noteKeys + // ); + // const updatedNoteKeyObj = noteKeyArrToObj(noteKeys); - useEffect(() => { - if (data && !hotTableDataUpdatesRef.current.initialized) { - hotTableDataUpdatesRef.current.initialized = true; - const noteKeys = noteKeyObjToArr(data.note_keys); - const { hotData, hotDataToStudyMapping } = annotationNotesToHotData( - noteKeys, - data.notes as NoteCollectionReturn[] | undefined, - (annotationNote) => { - const studyName = - annotationNote.study_name && annotationNote.study_year - ? `(${annotationNote.study_year}) ${annotationNote.study_name}` - : annotationNote.study_name - ? annotationNote.study_name - : ''; + // mutate( + // { + // argAnnotationId: props.annotationId, + // annotation: { + // notes: updatedAnnotationNotes.map((annotationNote) => ({ + // note: annotationNote.note, + // analysis: annotationNote.analysis, + // study: annotationNote.study, + // })), + // note_keys: updatedNoteKeyObj, + // }, + // }, + // { + // onSuccess: () => { + // setAnnotationIsEdited(false); + // }, + // } + // ); + // }; - const analysisName = annotationNote.analysis_name || ''; - - return [studyName, analysisName]; - } - ); - - setInitialAnnotationHotState({ - hotDataToStudyMapping, - noteKeys, - hotColumns: createColumns(noteKeys), - hotData: hotData, - mergeCells: getMergeCells(hotDataToStudyMapping), - }); - } - }, [data]); - - const handleClickSave = () => { - if (!props.annotationId) return; - if (!theLoggedInUserOwnsThisAnnotation) { - enqueueSnackbar('You do not have permission to edit this annotation', { - variant: 'error', - }); - return; - } - - const { hotData, noteKeys } = hotTableDataUpdatesRef.current; - - const updatedAnnotationNotes = hotDataToAnnotationNotes( - hotData, - initialAnnotationHotState.hotDataToStudyMapping, - noteKeys - ); - const updatedNoteKeyObj = noteKeyArrToObj(noteKeys); - - mutate( - { - argAnnotationId: props.annotationId, - annotation: { - notes: updatedAnnotationNotes.map((annotationNote) => ({ - note: annotationNote.note, - analysis: annotationNote.analysis, - study: annotationNote.study, - })), - note_keys: updatedNoteKeyObj, - }, - }, - { - onSuccess: () => { - setAnnotationIsEdited(false); - }, - } - ); - }; - - const handleChange = useCallback( - (hotData: AnnotationNoteValue[][], noteKeys: NoteKeyType[]) => { - setAnnotationIsEdited(true); - hotTableDataUpdatesRef.current = { - ...hotTableDataUpdatesRef.current, - hotData, - noteKeys, - }; - }, - [] - ); + // const handleChange = useCallback( + // (hotData: AnnotationNoteValue[][], noteKeys: NoteKeyType[]) => { + // setAnnotationIsEdited(true); + // hotTableDataUpdatesRef.current = { + // ...hotTableDataUpdatesRef.current, + // hotData, + // noteKeys, + // }; + // }, + // [] + // ); return ( - - - - + <> + // + // {/* */} + // + // ); }; diff --git a/compose/neurosynth-frontend/src/components/EditStudyComponents/EditAnalyses/EditAnalysis/EditAnalysis.tsx b/compose/neurosynth-frontend/src/components/EditStudyComponents/EditAnalyses/EditAnalysis/EditAnalysis.tsx index 05de0a272..82e2d3026 100644 --- a/compose/neurosynth-frontend/src/components/EditStudyComponents/EditAnalyses/EditAnalysis/EditAnalysis.tsx +++ b/compose/neurosynth-frontend/src/components/EditStudyComponents/EditAnalyses/EditAnalysis/EditAnalysis.tsx @@ -2,12 +2,11 @@ import HelpIcon from '@mui/icons-material/Help'; import { Box, Button, Tooltip, Typography } from '@mui/material'; import ConfirmationDialog from 'components/Dialogs/ConfirmationDialog/ConfirmationDialog'; import DisplayAnalysisWarnings from 'components/DisplayStudy/DisplayAnalyses/DisplayAnalysisWarnings/DisplayAnalysisWarnings'; +import EditAnalysisDetails from 'components/EditStudyComponents/EditAnalyses/EditAnalysisDetails/EditAnalysisDetails'; import { useDeleteAnalysis } from 'pages/Studies/StudyStore'; import { useState } from 'react'; -import EditAnalysisDetails from 'components/EditStudyComponents/EditAnalyses/EditAnalysisDetails/EditAnalysisDetails'; -import EditAnalysisPoints from 'components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPoints'; -import EditAnalysisPointSpaceAndStatistic from 'components/EditStudyComponents/EditAnalyses/EditAnalysisPoints/EditAnalysisPointSpaceAndStatistic/EditAnalysisPointSpaceAndStatistic'; import { useDeleteAnnotationNote } from 'stores/AnnotationStore.actions'; +import EditAnalysisPoints from '../EditAnalysisPoints/EditAnalysisPoints/EditAnalysisPoints'; const EditAnalysis: React.FC<{ analysisId?: string; onDeleteAnalysis: () => void }> = (props) => { const deleteAnalysis = useDeleteAnalysis(); @@ -31,28 +30,8 @@ const EditAnalysis: React.FC<{ analysisId?: string; onDeleteAnalysis: () => void return ( - - - Analysis Details - - - - - - - - Analysis Coordinates - - - - - - - - + + {/* 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 */} {/* diff --git a/compose/neurosynth-frontend/src/components/EditStudyComponents/EditAnalyses/EditAnalysisDetails/EditAnalysisDetails.tsx b/compose/neurosynth-frontend/src/components/EditStudyComponents/EditAnalyses/EditAnalysisDetails/EditAnalysisDetails.tsx index 7ffb998e4..f600f9f27 100644 --- a/compose/neurosynth-frontend/src/components/EditStudyComponents/EditAnalyses/EditAnalysisDetails/EditAnalysisDetails.tsx +++ b/compose/neurosynth-frontend/src/components/EditStudyComponents/EditAnalyses/EditAnalysisDetails/EditAnalysisDetails.tsx @@ -1,4 +1,4 @@ -import { Box, TextField } from '@mui/material'; +import { Box, TextField, Typography } from '@mui/material'; import { useAddOrUpdateAnalysis, useStudyAnalysisDescription, @@ -25,6 +25,9 @@ const EditAnalysisDetails: React.FC<{ analysisId?: string }> = (props) => { return ( + + Analysis Details + = (props) => { + return ( + + + + Analysis Coordinates + + + + + + + + + ); +}; + +export default EditAnalysisPoints; diff --git a/compose/neurosynth-frontend/src/components/EditStudyComponents/EditStudyAnnotations/EditStudyAnnotations.tsx b/compose/neurosynth-frontend/src/components/EditStudyComponents/EditStudyAnnotations/EditStudyAnnotations.tsx index 11ff87886..39ce5934f 100644 --- a/compose/neurosynth-frontend/src/components/EditStudyComponents/EditStudyAnnotations/EditStudyAnnotations.tsx +++ b/compose/neurosynth-frontend/src/components/EditStudyComponents/EditStudyAnnotations/EditStudyAnnotations.tsx @@ -1,120 +1,15 @@ import { Box, Typography } from '@mui/material'; -import AnnotationsHotTable from 'components/HotTables/AnnotationsHotTable/AnnotationsHotTable'; -import { - AnnotationNoteValue, - NoteKeyType, - annotationNotesToHotData, - createColumns, - hotDataToAnnotationNotes, -} from 'components/HotTables/helpers/utils'; import EditStudyComponentsStyles from 'components/EditStudyComponents/EditStudyComponents.styles'; +import EditStudyAnnotationsHotTable from 'components/HotTables/EditStudyAnnotationsHotTable/EditStudyAnnotationsHotTable'; import NeurosynthAccordion from 'components/NeurosynthAccordion/NeurosynthAccordion'; import StateHandlerComponent from 'components/StateHandlerComponent/StateHandlerComponent'; -import { DetailedSettings as MergeCellsSettings } from 'handsontable/plugins/mergeCells'; -import { ColumnSettings } from 'handsontable/settings'; -import { useGetAnnotationById } from 'hooks'; -import { NoteCollectionReturn } from 'neurostore-typescript-sdk'; -import { useProjectExtractionAnnotationId } from 'pages/Projects/ProjectPage/ProjectStore'; import { useStudyAnalyses } from 'pages/Studies/StudyStore'; -import { useCallback, useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; -import { - useAnnotationNoteKeys, - useSetAnnotationIsEdited, - useUpdateAnnotationNotes, -} from 'stores/AnnotationStore.actions'; -import { useAnnotationNotes } from 'stores/AnnotationStore.getters'; -import EditStudyAnnotationsHotTable from 'components/HotTables/EditStudyAnnotationsHotTable/EditStudyAnnotationsHotTable'; - -const hardCodedColumns = ['Analysis', 'Description']; +import { useAnnotationIsError, useAnnotationIsLoading } from 'stores/AnnotationStore.getters'; const EditStudyAnnotations: React.FC = (props) => { - const { studyId } = useParams<{ studyId: string }>(); - const annotationId = useProjectExtractionAnnotationId(); const analyses = useStudyAnalyses(); - const { data: annotation, isLoading, isError } = useGetAnnotationById(annotationId); - const setAnnotationIsEdited = useSetAnnotationIsEdited(); - const updateAnnotationNotes = useUpdateAnnotationNotes(); - - const notes = useAnnotationNotes(); - const noteKeys = useAnnotationNoteKeys(); - - const [initialAnnotationHotState, setInitialAnnotationHotState] = useState<{ - hotDataToStudyMapping: Map; - noteKeys: NoteKeyType[]; - hotData: AnnotationNoteValue[][]; - hotColumns: ColumnSettings[]; - mergeCells: MergeCellsSettings[]; - size: string; - }>({ - hotDataToStudyMapping: new Map(), - noteKeys: [], - hotData: [], - hotColumns: [], - mergeCells: [], - size: '300px', - }); - // CURRTODO: i need to refactor this - give edit study annotations its own HotTable and make it robust to many rerenders - // this means using a setState and storing tabularData in the store - - // useEffect(() => { - // if (annotation) { - // const annotationNotesForStudy = ((notes as NoteCollectionReturn[]) || []).filter( - // (x) => x.study === studyId - // ); - // const { hotData, hotDataToStudyMapping } = annotationNotesToHotData( - // noteKeys, - // annotationNotesForStudy, - // (annotationNote) => { - // const analysis = analyses.find((x) => x.id === annotationNote.analysis); - // return [analysis?.name || '', analysis?.description || '']; - // } - // ); - - // setInitialAnnotationHotState({ - // hotDataToStudyMapping, - // noteKeys, - // hotColumns: createColumns(noteKeys), - // hotData: hotData, - // mergeCells: [], - // size: `${(hotData.length + 1) * 35 > 400 ? 400 : (hotData.length + 1) * 35}px`, - // }); - // } - // }, [studyId, analyses, annotation, notes, noteKeys]); - - const handleChange = useCallback( - (hotData: AnnotationNoteValue[][], noteKeys: NoteKeyType[]) => { - const convertedAnnotationNotes = hotDataToAnnotationNotes( - hotData, - initialAnnotationHotState.hotDataToStudyMapping, - noteKeys - ); - - const updatedAnnotationNotes = ( - (annotation?.notes || []) as NoteCollectionReturn[] - ).map((annotationNote) => { - const annotationNoteEdited = convertedAnnotationNotes.find( - (x) => x.analysis === annotationNote.analysis - ); - // if we have not found it (i.e. the annotation is not part of the study annotations we are editing) then we just return a copy of the original. - // if we have found it, (i.e. the annotation is part of the study annotations we are editing) then we return the version we have edited - if (!annotationNoteEdited) { - return { ...annotationNote }; - } else { - return { ...annotationNoteEdited }; - } - }); - - updateAnnotationNotes(updatedAnnotationNotes); - setAnnotationIsEdited(true); - }, - [ - annotation?.notes, - initialAnnotationHotState.hotDataToStudyMapping, - setAnnotationIsEdited, - updateAnnotationNotes, - ] - ); + const isLoading = useAnnotationIsLoading(); + const isError = useAnnotationIsError(); return ( { } > - - {analyses.length === 0 ? ( - - There are no annotations for this study. To get started, add an analysis - below - - ) : ( - - )} - {/* */} - + {analyses.length === 0 ? ( + + There are no annotations for this study. To get started, add an analysis + below + + ) : ( + + )} ); diff --git a/compose/neurosynth-frontend/src/components/HotTables/AnnotationsHotTable/AnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/components/HotTables/AnnotationsHotTable/AnnotationsHotTable.tsx deleted file mode 100644 index f2e7d39db..000000000 --- a/compose/neurosynth-frontend/src/components/HotTables/AnnotationsHotTable/AnnotationsHotTable.tsx +++ /dev/null @@ -1,274 +0,0 @@ -import HotTable, { HotTableProps } from '@handsontable/react'; -import { Box, Typography } from '@mui/material'; -import { IMetadataRowModel, getType } from 'components/EditMetadata'; -import AddMetadataRow from 'components/EditMetadata/EditMetadataRow/AddMetadataRow'; -import { CellChange, ChangeSource } from 'handsontable/common'; -import { registerAllModules } from 'handsontable/registry'; -import { ColumnSettings } from 'handsontable/settings'; -import { useCallback, useEffect, useRef } from 'react'; -import { DetailedSettings } from 'handsontable/plugins/mergeCells'; -import { AnnotationNoteValue, NoteKeyType } from '../helpers/utils'; -import { CellCoords } from 'handsontable'; -import React from 'react'; -import { createColumnHeader, createColumns } from '../helpers/utils'; -import AnnotationsHotTableStyles from 'components/HotTables/AnnotationsHotTable/AnnotationsHotTable.styles'; - -const hotSettings: HotTableProps = { - fillHandle: false, - licenseKey: 'non-commercial-and-evaluation', - contextMenu: false, - viewportRowRenderingOffset: 2, - viewportColumnRenderingOffset: 100, // we do not want column virtualization as it screws up the spreadsheet - width: '100%', - fixedColumnsStart: 2, -}; - -const convertRemToPx = (rem: number) => { - return rem * parseFloat(getComputedStyle(document.documentElement).fontSize); -}; - -registerAllModules(); - -/** - * Note: this component preserves the state of the handsontable when manipulating data for performance reasons. - * As a result, it avoids rerenders. - */ -const AnnotationsHotTable: React.FC<{ - allowAddColumn?: boolean; - hardCodedReadOnlyCols: string[]; - allowRemoveColumns?: boolean; - onChange: (hotData: AnnotationNoteValue[][], updatedNoteKeys: NoteKeyType[]) => void; - hotData: AnnotationNoteValue[][]; - noteKeys: NoteKeyType[]; - mergeCells: DetailedSettings[]; - hotColumns: ColumnSettings[]; - stretchH?: 'all' | 'last' | ''; - wordWrap?: boolean; - size: string; -}> = React.memo((props) => { - const hotTableRef = useRef(null); - const hotStateRef = useRef<{ - noteKeys: NoteKeyType[]; - }>({ - noteKeys: [], - }); - const { noteKeys, hotData, mergeCells, onChange, hotColumns, hardCodedReadOnlyCols } = props; - useEffect(() => { - // make a copy as we don't want to modify the original - hotStateRef.current.noteKeys = noteKeys.map((x) => ({ ...x })); - }, [noteKeys]); - - // set handsontable ref height if the (debounced) window height changes. - // Must do this via an eventListener to avoid react re renders clearing the HOT State - useEffect(() => { - let timeout: any; - let originalHOTHeight: number; - const handleResize = () => { - const currentWindowSize = window.innerHeight; - if (!currentWindowSize) return; - if (timeout) clearTimeout(timeout); - timeout = setTimeout(async () => { - if (!hotTableRef.current?.hotInstance) return; - - if (props.size === 'fitToPage') { - const navHeight = 64; - const breadCrumbHeight = 44; - const addMetadataHeightPx = 40 + 25; - const addMetadataHeightRem = 1; - const pageMarginsRem = 4; - const saveButton = 43; - const tablePaddingRem = 1; - - const spaceTakenBySpreadsheetWithManyRows = - currentWindowSize - - navHeight - - breadCrumbHeight - - convertRemToPx(addMetadataHeightRem) - - addMetadataHeightPx - - convertRemToPx(pageMarginsRem) - - saveButton - - convertRemToPx(tablePaddingRem); - - const currHotTableHeight = - document.getElementsByClassName('hot-container')[0]?.clientHeight || 0; - - // update original hotTableHeight with the initial non zero value - if (originalHOTHeight === undefined && currHotTableHeight > 0) { - originalHOTHeight = currHotTableHeight; - } - - if ( - currHotTableHeight > spaceTakenBySpreadsheetWithManyRows || // if the initial table exceeds the size of the page space - currHotTableHeight < originalHOTHeight // for the case where we make the window bigger but its still smaller than the initial table size - ) { - hotTableRef.current.hotInstance.updateSettings({ - height: spaceTakenBySpreadsheetWithManyRows, - }); - } else { - hotTableRef.current.hotInstance.updateSettings({ - height: originalHOTHeight, - }); - } - } else { - hotTableRef.current.hotInstance.updateSettings({ - height: `calc(${props.size})`, - }); - } - }, 200); - }; - window.addEventListener('resize', handleResize); - handleResize(); - - return () => { - if (timeout) clearTimeout(timeout); - window.removeEventListener('resize', handleResize); - }; - }, [props.size]); - - const handleRemoveHotColumn = useCallback( - (colKey: string) => { - if (!hotTableRef.current?.hotInstance) return; - - const foundIndex = hotStateRef.current.noteKeys.findIndex((x) => x.key === colKey); - if (foundIndex < 0) return; - - const noteKeys = hotStateRef.current.noteKeys; - const colHeaders = hotTableRef.current.hotInstance.getColHeader() as string[]; - const data = hotTableRef.current.hotInstance.getData() as AnnotationNoteValue[][]; - - noteKeys.splice(foundIndex, 1); - const columns = createColumns(noteKeys); - - colHeaders.splice(foundIndex + 2, 1); - data.forEach((row) => { - row.splice(foundIndex + 2, 1); - }); - - hotTableRef.current.hotInstance.updateSettings({ - data: data, - colHeaders: colHeaders, - columns: columns, - }); - - onChange( - hotTableRef.current.hotInstance.getData() as AnnotationNoteValue[][], - noteKeys - ); - }, - [onChange] - ); - - const handleCellMouseUp = ( - event: MouseEvent, - coords: CellCoords, - TD: HTMLTableCellElement - ): void => { - const target = event.target as HTMLButtonElement; - if (coords.row < 0 && (target.tagName === 'svg' || target.tagName === 'path')) { - handleRemoveHotColumn(TD.innerText); - } - }; - - const handleAddHotColumn = (row: IMetadataRowModel) => { - if (!hotTableRef.current?.hotInstance) return false; - - const trimmedKey = row.metadataKey.trim(); - - if (hotStateRef.current.noteKeys.find((x) => x.key === trimmedKey)) return false; - - const noteKeys = hotStateRef.current.noteKeys; - const colHeaders = hotTableRef.current.hotInstance.getColHeader() as string[]; - const data = hotTableRef.current.hotInstance.getData() as AnnotationNoteValue[][]; - - noteKeys.unshift({ key: trimmedKey, type: getType(row.metadataValue) }); - const columns = createColumns(noteKeys); - - colHeaders.splice( - 2, - 0, - createColumnHeader( - trimmedKey, - getType(row.metadataValue), - props.allowRemoveColumns || false - ) - ); - - data.forEach((row) => { - row.splice(2, 0, null); - }); - - hotTableRef.current.hotInstance.updateSettings({ - data: data, - colHeaders: colHeaders, - columns: columns, - }); - - onChange(hotTableRef.current.hotInstance.getData() as AnnotationNoteValue[][], noteKeys); - - return true; - }; - - const handleChangeOccurred = (changes: CellChange[] | null, source: ChangeSource) => { - // this hook is triggered during merge cells and on initial update. We don't want the parent to be notified unless its a real user change - if (!changes || changes.some((x) => x[1] === 0)) return; - if (hotTableRef.current?.hotInstance) { - const hotData = hotTableRef.current.hotInstance.getData() as AnnotationNoteValue[][]; - onChange(hotData, hotStateRef.current.noteKeys); - } - }; - - const initialHotColumnHeaders = [ - ...hardCodedReadOnlyCols, - ...noteKeys.map((col) => - createColumnHeader( - col.key, - col.type, - !!props.allowRemoveColumns && col.key !== 'included' - ) - ), - ]; - - return ( - - {props.allowAddColumn && ( - - - - )} - - {hotData.length > 0 ? ( - - ) : ( - - There are no analyses to annotate. Get started by adding analyses to your - studies. - - )} - - - ); -}); - -export default AnnotationsHotTable; diff --git a/compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPoints.helpers.ts b/compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPointsHotTable.helpers.ts similarity index 96% rename from compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPoints.helpers.ts rename to compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPointsHotTable.helpers.ts index ef98130aa..678b15480 100644 --- a/compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPoints.helpers.ts +++ b/compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPointsHotTable.helpers.ts @@ -1,6 +1,6 @@ import { HotTableProps } from '@handsontable/react'; import { CellValue } from 'handsontable/common'; -import styles from 'components/HotTables/AnnotationsHotTable/AnnotationsHotTable.module.css'; +import styles from 'components/HotTables/HotTables.module.css'; import { ColumnSettings } from 'handsontable/settings'; const nonEmptyNumericValidator = (value: CellValue, callback: (isValid: boolean) => void) => { diff --git a/compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPoints.styles.ts b/compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPointsHotTable.styles.ts similarity index 100% rename from compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPoints.styles.ts rename to compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPointsHotTable.styles.ts diff --git a/compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPoints.tsx b/compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPointsHotTable.tsx similarity index 81% rename from compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPoints.tsx rename to compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPointsHotTable.tsx index 2cd4461a6..975c183ab 100644 --- a/compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPoints.tsx +++ b/compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/EditAnalysisPointsHotTable.tsx @@ -12,20 +12,19 @@ import { useUpdateAnalysisPoints, } from 'pages/Studies/StudyStore'; import { IStorePoint } from 'pages/Studies/StudyStore.helpers'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useRef } from 'react'; +import { sanitizePaste } from '../HotTables.utils'; import { EditAnalysisPointsDefaultConfig, getHotTableInsertionIndices, hotTableColHeaders, hotTableColumnSettings, -} from './EditAnalysisPoints.helpers'; -import { sanitizePaste } from '../helpers/utils'; - -export const ROW_HEIGHT = 56; +} from './EditAnalysisPointsHotTable.helpers'; +import useEditAnalysisPointsHotTable from './useEditAnalysisPointsHotTable'; registerAllModules(); -const EditAnalysisPoints: React.FC<{ analysisId?: string }> = React.memo((props) => { +const EditAnalysisPointsHotTable: React.FC<{ analysisId?: string }> = React.memo((props) => { const points = useStudyAnalysisPoints(props.analysisId) as IStorePoint[] | null; const updatePoints = useUpdateAnalysisPoints(); const createPoint = useCreateAnalysisPoints(); @@ -35,47 +34,11 @@ const EditAnalysisPoints: React.FC<{ analysisId?: string }> = React.memo((props) insertRowsAbove: true, insertedRowsViaPaste: [], }); - const [insertRowsDialogIsOpen, setInsertRowsDialogIsOpen] = useState(false); - - // run every time points are updated to validate (in charge of highlighting the cells that are invalid) - useEffect(() => { - hotTableRef.current?.hotInstance?.validateCells(); - }, [points]); - - // run once initially to set the custom context menu - useEffect(() => { - if (hotTableRef.current?.hotInstance) { - hotTableRef.current.hotInstance.updateSettings({ - contextMenu: { - items: { - row_above: { - name: 'Add rows above', - callback: (key, options) => { - hotTableMetadata.current.insertRowsAbove = true; - setInsertRowsDialogIsOpen(true); - }, - }, - row_below: { - name: 'Add rows below', - callback: (key, options) => { - hotTableMetadata.current.insertRowsAbove = false; - setInsertRowsDialogIsOpen(true); - }, - }, - remove_row: { - name: 'Remove row(s)', - }, - copy: { - name: 'Copy', - }, - cut: { - name: 'Cut', - }, - }, - }, - }); - } - }, [hotTableRef]); + const { height, insertRowsDialogIsOpen, closeInsertRowsDialog } = useEditAnalysisPointsHotTable( + props.analysisId, + hotTableRef, + hotTableMetadata + ); // handsontable binds and updates to the data references themselves which means the original data is being mutated. // as we use zustand, this may not be a good idea, so we implement handleAfterChange to @@ -201,13 +164,10 @@ const EditAnalysisPoints: React.FC<{ analysisId?: string }> = React.memo((props) })), hotTableMetadata.current.insertRowsAbove ? insertAboveIndex : insertBelowIndex + 1 ); - setInsertRowsDialogIsOpen(false); + closeInsertRowsDialog(); } }; - const totalHeight = 28 + (points?.length || 0) * 23; - const height = totalHeight > 500 ? 500 : totalHeight; - /** * Hook Order: * 1) handleBeforePaste @@ -221,7 +181,7 @@ const EditAnalysisPoints: React.FC<{ analysisId?: string }> = React.memo((props) setInsertRowsDialogIsOpen(false)} + onCloseDialog={() => closeInsertRowsDialog()} onInputNumber={(val) => handleInsertRows(val)} dialogDescription="" /> @@ -238,7 +198,7 @@ const EditAnalysisPoints: React.FC<{ analysisId?: string }> = React.memo((props) colHeaders={hotTableColHeaders} data={[...(points || [])]} /> - {!points || points.length === 0 ? ( + {(points?.length || 0) === 0 ? ( No coordinate data.{' '} = React.memo((props) ); }); -export default EditAnalysisPoints; +export default EditAnalysisPointsHotTable; diff --git a/compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/useEditAnalysisPointsHotTable.tsx b/compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/useEditAnalysisPointsHotTable.tsx new file mode 100644 index 000000000..645de3272 --- /dev/null +++ b/compose/neurosynth-frontend/src/components/HotTables/EditAnalysisPointsHotTable/useEditAnalysisPointsHotTable.tsx @@ -0,0 +1,80 @@ +import HotTable from '@handsontable/react'; +import { useStudyAnalysisPoints } from 'pages/Studies/StudyStore'; +import { IStorePoint } from 'pages/Studies/StudyStore.helpers'; +import { useEffect, useMemo, useState } from 'react'; + +const useAnalysisPointsHotTable = ( + aanlysisId: string | undefined, + hotTableRef: React.RefObject, + hotTableMetadata: React.MutableRefObject<{ + insertRowsAbove: boolean; + insertedRowsViaPaste: any[][]; + }> +) => { + const points = useStudyAnalysisPoints(aanlysisId) as IStorePoint[] | null; + const [insertRowsDialogIsOpen, setInsertRowsDialogIsOpen] = useState(false); + + const closeInsertRowsDialog = () => { + setInsertRowsDialogIsOpen(false); + }; + + const openInsertRowsDialog = () => { + setInsertRowsDialogIsOpen(true); + }; + + // run every time points are updated to validate (in charge of highlighting the cells that are invalid) + useEffect(() => { + hotTableRef.current?.hotInstance?.validateCells(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [points]); + + // run once initially to set the custom context menu + useEffect(() => { + if (hotTableRef.current?.hotInstance) { + hotTableRef.current.hotInstance.updateSettings({ + contextMenu: { + items: { + row_above: { + name: 'Add rows above', + callback: (key, options) => { + hotTableMetadata.current.insertRowsAbove = true; + openInsertRowsDialog(); + }, + }, + row_below: { + name: 'Add rows below', + callback: (key, options) => { + hotTableMetadata.current.insertRowsAbove = false; + openInsertRowsDialog(); + }, + }, + remove_row: { + name: 'Remove row(s)', + }, + copy: { + name: 'Copy', + }, + cut: { + name: 'Cut', + }, + }, + }, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hotTableRef]); + + const height = useMemo(() => { + const totalHeight = 28 + (points?.length || 0) * 23; + const height = totalHeight > 500 ? 500 : totalHeight; + return height; + }, [points]); + + return { + height, + insertRowsDialogIsOpen, + closeInsertRowsDialog, + }; +}; + +export default useAnalysisPointsHotTable; diff --git a/compose/neurosynth-frontend/src/components/HotTables/helpers/utils.tsx b/compose/neurosynth-frontend/src/components/HotTables/EditAnnotationsHotTable/EditAnnotationsHotTable.helpers.tsx similarity index 68% rename from compose/neurosynth-frontend/src/components/HotTables/helpers/utils.tsx rename to compose/neurosynth-frontend/src/components/HotTables/EditAnnotationsHotTable/EditAnnotationsHotTable.helpers.tsx index 5c1a896d1..c5ca65fa4 100644 --- a/compose/neurosynth-frontend/src/components/HotTables/helpers/utils.tsx +++ b/compose/neurosynth-frontend/src/components/HotTables/EditAnnotationsHotTable/EditAnnotationsHotTable.helpers.tsx @@ -1,71 +1,62 @@ +import { HotTableProps } from '@handsontable/react'; import { EPropertyType } from 'components/EditMetadata'; +import { AnnotationNoteValue, NoteKeyType } from 'components/HotTables/HotTables.types'; import { NoteCollectionReturn } from 'neurostore-typescript-sdk'; -import { DetailedSettings as MergeCellsSettings } from 'handsontable/plugins/mergeCells'; -import { ColumnSettings } from 'handsontable/settings'; -import { numericValidator } from 'handsontable/validators'; -import styles from 'components/HotTables/AnnotationsHotTable/AnnotationsHotTable.module.css'; -import { CellValue } from 'handsontable/common'; import { renderToString } from 'react-dom/server'; import Cancel from '@mui/icons-material/Cancel'; +import { DetailedSettings as MergeCellsSettings } from 'handsontable/plugins/mergeCells'; +import styles from 'components/HotTables/HotTables.module.css'; +import { numericValidator } from 'handsontable/validators'; +import { booleanValidator } from 'components/HotTables/HotTables.utils'; +import { ColumnSettings } from 'handsontable/settings'; -export interface NoteKeyType { - key: string; - type: EPropertyType; -} - -export type AnnotationNoteValue = string | number | boolean | null; - -export const noteKeyObjToArr = (noteKeys?: object | null): NoteKeyType[] => { - if (!noteKeys) return []; - const noteKeyTypes = noteKeys as { [key: string]: EPropertyType }; - const arr = Object.entries(noteKeyTypes).map(([key, type]) => ({ - key, - type, - })); - return arr; +export const hotSettings: HotTableProps = { + licenseKey: 'non-commercial-and-evaluation', + contextMenu: false, + viewportRowRenderingOffset: 4, + viewportColumnRenderingOffset: 4, // we do not want column virtualization as it screws up the spreadsheet + width: '100%', + fixedColumnsStart: 2, + wordWrap: true, + autoRowSize: true, + afterGetRowHeaderRenderers: (headerRenderers) => { + headerRenderers.push((row, TH) => { + TH.className = styles['no-top-bottom-borders']; + }); + }, + id: 'hot-annotations', + fillHandle: { + direction: 'vertical', + autoInsertRow: false, + }, }; -export const noteKeyArrToObj = (noteKeyArr: NoteKeyType[]): { [key: string]: EPropertyType } => { - const noteKeyObj: { [key: string]: EPropertyType } = noteKeyArr.reduce((acc, curr) => { - acc[curr.key] = curr.type; - return acc; - }, {} as { [key: string]: EPropertyType }); - - return noteKeyObj; +export const convertRemToPx = (rem: number) => { + return rem * parseFloat(getComputedStyle(document.documentElement).fontSize); }; -// we can assume that the input is already sorted -export const getMergeCells = ( - hotDataToStudyMapping: Map -) => { - const mergeCells: MergeCellsSettings[] = []; +export const hotDataToAnnotationNotes = ( + hotData: AnnotationNoteValue[][], + mapping: Map, + noteKeys: NoteKeyType[] +): NoteCollectionReturn[] => { + const noteCollections: NoteCollectionReturn[] = hotData.map((row, index) => { + const mappedStudyAnalysis = mapping.get(index) as { studyId: string; analysisId: string }; - let studyId: string; - let mergeCellObj: MergeCellsSettings = { - row: 0, - col: 0, - rowspan: 1, - colspan: 1, - }; - hotDataToStudyMapping.forEach((value, key) => { - if (value.studyId === studyId) { - mergeCellObj.rowspan++; - if (key === hotDataToStudyMapping.size - 1 && mergeCellObj.rowspan > 1) { - mergeCells.push(mergeCellObj); - } - } else { - if (mergeCellObj.rowspan > 1) mergeCells.push(mergeCellObj); - studyId = value.studyId; - mergeCellObj = { - row: key, - col: 0, - rowspan: 1, - colspan: 1, - }; + const updatedNote: { [key: string]: AnnotationNoteValue } = {}; + for (let i = 0; i < noteKeys.length; i++) { + const noteKey = noteKeys[i]; + updatedNote[noteKey.key] = row[i + 2]; // take into account the first two columns which are readonly reseved to display study name and analysis name } + + return { + study: mappedStudyAnalysis.studyId, + analysis: mappedStudyAnalysis.analysisId, + note: updatedNote, + }; }); - return mergeCells; + return noteCollections; }; export const annotationNotesToHotData = ( @@ -128,52 +119,43 @@ export const annotationNotesToHotData = ( }; }; -export const hotDataToAnnotationNotes = ( - hotData: AnnotationNoteValue[][], - mapping: Map, - noteKeys: NoteKeyType[] -): NoteCollectionReturn[] => { - const noteCollections: NoteCollectionReturn[] = hotData.map((row, index) => { - const mappedStudyAnalysis = mapping.get(index) as { studyId: string; analysisId: string }; - - const updatedNote: { [key: string]: AnnotationNoteValue } = {}; - for (let i = 0; i < noteKeys.length; i++) { - const noteKey = noteKeys[i]; - updatedNote[noteKey.key] = row[i + 2]; // take into account the first two columns which are readonly reseved to display study name and analysis name - } - - return { - study: mappedStudyAnalysis.studyId, - analysis: mappedStudyAnalysis.analysisId, - note: updatedNote, - }; - }); - - return noteCollections; -}; +export const createColumnHeader = ( + colKey: string, + colType: EPropertyType, + allowRemoveColumn: boolean +) => { + const allowRemove = allowRemoveColumn + ? `
+ ${renderToString( + + )} +
` + : '
'; -const booleanValidator = (value: CellValue, callback: (isValid: boolean) => void) => { - const isValid = - value === true || - value === false || - value === 'true' || - value === 'false' || - value === null || - value === ''; - callback(isValid); + return ( + `
` + + `
${colKey}
` + + allowRemove + + `
` + ); }; export const createColumns = (noteKeys: NoteKeyType[]) => [ { - className: `${styles['study-col']} ${styles['read-only-col']}`, + className: `${styles['study-col']} ${styles['read-only-col']} truncate`, readOnly: true, - width: '100', }, { className: styles['read-only-col'], readOnly: true, - width: '100', }, ...noteKeys.map((x) => { return { @@ -191,56 +173,36 @@ export const createColumns = (noteKeys: NoteKeyType[]) => }), ] as ColumnSettings[]; -export const createColumnHeader = (colKey: string, colType: EPropertyType, canUpdate: boolean) => { - const allowRemove = canUpdate - ? `
- ${renderToString( - - )} -
` - : '
'; - - return ( - `
` + - `
${colKey}
` + - allowRemove + - `
` - ); -}; - -export const replaceString = (val: string) => { - // replace = ['֊', '‐', '‑', '⁃', '﹣', '-', '‒', '–', '—', '﹘', '−', '-'] - - return val.replaceAll(new RegExp('֊|‐|‑|⁃|﹣|-|‒|–|—|﹘|−|-', 'g'), '-'); -}; - -export const stripTags = (stringWhichMayHaveHTML: any) => { - if (typeof stringWhichMayHaveHTML !== 'string') return ''; - - let doc = new DOMParser().parseFromString(stringWhichMayHaveHTML, 'text/html'); - return doc.body.textContent || ''; -}; - -export const sanitizePaste = (data: any[][]) => { - data.forEach((dataRow, rowIndex) => { - dataRow.forEach((value, valueIndex) => { - if (typeof value === 'number') return; - if (typeof value === 'boolean') return; - - let newVal = value; - newVal = stripTags(newVal); // strip all HTML tags that were copied over if they exist - newVal = replaceString(newVal); // replace minus operator with javascript character code +// we can assume that the input is already sorted +export const getMergeCells = ( + hotDataToStudyMapping: Map +) => { + const mergeCells: MergeCellsSettings[] = []; - if (newVal === 'true') newVal = true; - else if (newVal === 'false') newVal = false; - data[rowIndex][valueIndex] = newVal; - }); + let studyId: string; + let mergeCellObj: MergeCellsSettings = { + row: 0, + col: 0, + rowspan: 1, + colspan: 1, + }; + hotDataToStudyMapping.forEach((value, key) => { + if (value.studyId === studyId) { + mergeCellObj.rowspan++; + if (key === hotDataToStudyMapping.size - 1 && mergeCellObj.rowspan > 1) { + mergeCells.push(mergeCellObj); + } + } else { + if (mergeCellObj.rowspan > 1) mergeCells.push(mergeCellObj); + studyId = value.studyId; + mergeCellObj = { + row: key, + col: 0, + rowspan: 1, + colspan: 1, + }; + } }); + + return mergeCells; }; diff --git a/compose/neurosynth-frontend/src/components/HotTables/AnnotationsHotTable/AnnotationsHotTable.styles.ts b/compose/neurosynth-frontend/src/components/HotTables/EditAnnotationsHotTable/EditAnnotationsHotTable.styles.ts similarity index 100% rename from compose/neurosynth-frontend/src/components/HotTables/AnnotationsHotTable/AnnotationsHotTable.styles.ts rename to compose/neurosynth-frontend/src/components/HotTables/EditAnnotationsHotTable/EditAnnotationsHotTable.styles.ts diff --git a/compose/neurosynth-frontend/src/components/HotTables/EditAnnotationsHotTable/EditAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/components/HotTables/EditAnnotationsHotTable/EditAnnotationsHotTable.tsx new file mode 100644 index 000000000..647cd1444 --- /dev/null +++ b/compose/neurosynth-frontend/src/components/HotTables/EditAnnotationsHotTable/EditAnnotationsHotTable.tsx @@ -0,0 +1,294 @@ +import HotTable from '@handsontable/react'; +import { Box, Typography } from '@mui/material'; +import LoadingButton from 'components/Buttons/LoadingButton/LoadingButton'; +import { IMetadataRowModel, getType } from 'components/EditMetadata'; +import AddMetadataRow from 'components/EditMetadata/EditMetadataRow/AddMetadataRow'; +import AnnotationsHotTableStyles from 'components/HotTables/EditAnnotationsHotTable/EditAnnotationsHotTable.styles'; +import useEditAnnotationsHotTable from 'components/HotTables/EditAnnotationsHotTable/useEditAnnotationsHotTable'; +import { NoteKeyType } from 'components/HotTables/HotTables.types'; +import { noteKeyArrToObj } from 'components/HotTables/HotTables.utils'; +import { CellCoords } from 'handsontable'; +import { CellChange } from 'handsontable/common'; +import { registerAllModules } from 'handsontable/registry'; +import { useGetWindowHeight, useUpdateAnnotationById } from 'hooks'; +import { useSnackbar } from 'notistack'; +import React, { useEffect, useRef } from 'react'; +import { + createColumns, + hotDataToAnnotationNotes, + hotSettings, +} from 'components/HotTables/EditAnnotationsHotTable/EditAnnotationsHotTable.helpers'; +import { SelectionController } from 'handsontable/selection'; + +registerAllModules(); + +const AnnotationsHotTable: React.FC<{ annotationId?: string }> = React.memo((props) => { + const { enqueueSnackbar } = useSnackbar(); + const { mutate, isLoading: updateAnnotationIsLoading } = useUpdateAnnotationById( + props.annotationId + ); + const hotTableRef = useRef(null); + const hotStateRef = useRef<{ + noteKeys: NoteKeyType[]; + }>({ + noteKeys: [], + }); + const windowSize = useGetWindowHeight(); + const { + hotColumnHeaders, + theUserOwnsThisAnnotation, + setAnnotationsHotState, + hotData, + mergeCells, + colWidths, + hotColumns, + noteKeys, + hotDataToStudyMapping, + isEdited, + } = useEditAnnotationsHotTable(props.annotationId); + + useEffect(() => { + let timeout: any = setTimeout(() => { + if (!hotTableRef.current?.hotInstance) return; + const sizes = [ + '64px', // NAV_HEIGHT + '3rem', // MARGIN_SPACING + '40px', // BREADCRUMBS + '1rem', // ADD_METADATA_INPUT_MARGIN_TOP + '40px', // ADD_METADATA_INPUT + '25px', // ADD_METADATA_INPUT_MARGIN_BOTTOM + '75px', // BOTTOM_BUTTON_CONTAINER + '1rem', // EXTRA SPACE + ]; + const sizeStr = sizes.reduce((acc, curr, index, list) => { + if (index === 0) { + return `calc(${windowSize}px - ${curr} - `; + } else if (index === list.length - 1) { + return `${acc}${curr})`; + } else { + return `${acc}${curr} - `; + } + }, ''); + + hotTableRef.current.hotInstance.updateSettings({ + height: sizeStr, + }); + }, 400); + + return () => { + if (timeout) clearTimeout(timeout); + }; + }, [windowSize]); + + const handleClickSave = () => { + if (!props.annotationId) return; + if (!theUserOwnsThisAnnotation) { + enqueueSnackbar('You do not have permission to edit this annotation', { + variant: 'error', + }); + return; + } + + const updatedAnnotationNotes = hotDataToAnnotationNotes( + hotData, + hotDataToStudyMapping, + noteKeys + ); + const updatedNoteKeyObj = noteKeyArrToObj(noteKeys); + + mutate( + { + argAnnotationId: props.annotationId, + annotation: { + notes: updatedAnnotationNotes.map((annotationNote) => ({ + note: annotationNote.note, + analysis: annotationNote.analysis, + study: annotationNote.study, + })), + note_keys: updatedNoteKeyObj, + }, + }, + { + onSuccess: () => { + setAnnotationsHotState((prev) => ({ ...prev, isEdited: false })); + }, + } + ); + }; + + const handleRemoveHotColumn = (colKey: string) => { + const foundIndex = noteKeys.findIndex((x) => x.key === colKey && x.key !== 'included'); + if (foundIndex < 0) return; + + setAnnotationsHotState((prev) => { + const updatedNoteKeys = [...prev.noteKeys]; + updatedNoteKeys.splice(foundIndex, 1); + + return { + ...prev, + isEdited: true, + noteKeys: updatedNoteKeys, + hotColumns: createColumns(updatedNoteKeys), + hotData: [...prev.hotData].map((row) => { + const updatedRow = [...row]; + updatedRow.splice(foundIndex + 2, 1); + return updatedRow; + }), + }; + }); + }; + + const handleCellMouseUp = ( + event: MouseEvent, + coords: CellCoords, + TD: HTMLTableCellElement + ): void => { + const target = event.target as HTMLButtonElement; + if (coords.row < 0 && (target.tagName === 'svg' || target.tagName === 'path')) { + handleRemoveHotColumn(TD.innerText); + } + }; + + /** + * NOTE: there is a bug where fixed, mergedCells (such as the cells showing our studies) get messed up when you scroll to the right. I think that this is + * due to virtualization - as we scroll to the right, the original heights of the cells are no longer in the DOM and so the calculated row heights are lost and + * they revert to the default. + * + * What ended up fixing this issue was adding row headers...I think this is because their heights are calculated and maintained regardless of virtualization. + * In conclusion, implementing the following solved this issue: + * 1. adding autoRowSize: true + * 2. implementing afterGetRowHeaderRenderers to remove the top and bottom borders for stylistic reasons as they dont look good next to the merged cells + * the row headers themselves are not merged + * 3. add handleCellMouseDown to prevent the user from selecting an entire row - for stylistic reasons but also theres no reason for them to select a row + */ + const handleCellMouseDown = ( + event: MouseEvent, + coords: CellCoords, + TD: HTMLTableCellElement, + controller: SelectionController + ): void => { + const isRowHeader = coords.col === -1; + if (isRowHeader) { + event.stopImmediatePropagation(); + return; + } + }; + + const handleAddHotColumn = (row: IMetadataRowModel) => { + const trimmedKey = row.metadataKey.trim(); + if (noteKeys.find((x) => x.key === trimmedKey)) return false; + + setAnnotationsHotState((prev) => { + const updatedNoteKeys = [ + { key: trimmedKey, type: getType(row.metadataValue) }, + ...prev.noteKeys, + ]; + + return { + ...prev, + isEdited: true, + noteKeys: updatedNoteKeys, + hotColumns: createColumns(updatedNoteKeys), + hotData: [...prev.hotData].map((row) => { + const updatedRow = [...row]; + updatedRow.splice(2, 0, null); + return updatedRow; + }), + }; + }); + + return true; + }; + + /** + * On top of being triggered when a change occurs, this hook is also triggered during initial mergeCells and on initial update. + */ + const handleChangeOccurred = (changes: CellChange[] | null, source: any) => { + if (!changes) return; + const isDoingMergeCellOperation = changes.some((x) => x[1] === 0); + if (isDoingMergeCellOperation) return; // We don't want update to occur when handsontable is merging cells, only when a user update occurs + + setAnnotationsHotState((prev) => { + const updatedHotData = [...prev.hotData]; + changes.forEach(([row, col, _valChangedFrom, valChangedTo]) => { + updatedHotData[row] = [...updatedHotData[row]]; + updatedHotData[row][col as number] = valChangedTo; + }); + + return { + ...prev, + hotData: updatedHotData, + isEdited: true, + }; + }); + }; + + return ( + + {theUserOwnsThisAnnotation && ( + + + + )} + + {hotData.length > 0 ? ( + + ) : ( + + There are no analyses to annotate. Get started by adding analyses to your + studies. + + )} + + + + + + ); +}); + +export default AnnotationsHotTable; diff --git a/compose/neurosynth-frontend/src/components/HotTables/EditAnnotationsHotTable/useEditAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/components/HotTables/EditAnnotationsHotTable/useEditAnnotationsHotTable.tsx new file mode 100644 index 000000000..66bce5bee --- /dev/null +++ b/compose/neurosynth-frontend/src/components/HotTables/EditAnnotationsHotTable/useEditAnnotationsHotTable.tsx @@ -0,0 +1,99 @@ +import { useAuth0 } from '@auth0/auth0-react'; +import { createColWidths, noteKeyObjToArr } from 'components/HotTables/HotTables.utils'; +import { ColumnSettings } from 'handsontable/settings'; +import { useGetAnnotationById } from 'hooks'; +import { useEffect, useMemo, useState } from 'react'; +import { DetailedSettings as MergeCellsSettings } from 'handsontable/plugins/mergeCells'; +import { NoteCollectionReturn } from 'neurostore-typescript-sdk'; +import { AnnotationNoteValue, NoteKeyType } from 'components/HotTables/HotTables.types'; +import { + annotationNotesToHotData, + createColumnHeader, + getMergeCells, + createColumns, +} from 'components/HotTables/EditAnnotationsHotTable/EditAnnotationsHotTable.helpers'; + +const useEditAnnotationsHotTable = (annotationId?: string) => { + const { + data: annotations, + isLoading: getAnnotationIsLoading, + isError: getAnnotationIsError, + } = useGetAnnotationById(annotationId); + const { user } = useAuth0(); + const [annotationsHotState, setAnnotationsHotState] = useState<{ + hotDataToStudyMapping: Map; + noteKeys: NoteKeyType[]; + hotData: AnnotationNoteValue[][]; + hotColumns: ColumnSettings[]; + mergeCells: MergeCellsSettings[]; + isEdited: boolean; + }>({ + hotDataToStudyMapping: new Map(), + noteKeys: [], + hotData: [], + hotColumns: [], + mergeCells: [], + isEdited: false, + }); + + useEffect(() => { + if (!annotations) return; + + const noteKeys = noteKeyObjToArr(annotations.note_keys); + const { hotData, hotDataToStudyMapping } = annotationNotesToHotData( + noteKeys, + annotations.notes as NoteCollectionReturn[] | undefined, + (annotationNote) => { + const studyName = + annotationNote.study_name && annotationNote.study_year + ? `(${annotationNote.study_year}) ${annotationNote.study_name}` + : annotationNote.study_name + ? annotationNote.study_name + : ''; + + const analysisName = annotationNote.analysis_name || ''; + + return [studyName, analysisName]; + } + ); + + setAnnotationsHotState({ + hotDataToStudyMapping, + noteKeys, + hotColumns: createColumns(noteKeys), + hotData: hotData, + mergeCells: getMergeCells(hotDataToStudyMapping), + isEdited: false, + }); + }, [annotations]); + + const theUserOwnsThisAnnotation = useMemo(() => { + return (user?.sub || null) === (annotations?.user || undefined); + }, [user?.sub, annotations?.user]); + + const hotColumnHeaders = useMemo(() => { + return [ + ...['Study', 'Analysis'], + ...annotationsHotState.noteKeys.map((col) => { + const allowRemoveColumn = col.key !== 'included'; + return createColumnHeader(col.key, col.type, allowRemoveColumn); + }), + ]; + }, [annotationsHotState.noteKeys]); + + const colWidths = useMemo(() => { + return createColWidths(annotationsHotState.noteKeys, 200, 150, 200); + }, [annotationsHotState.noteKeys]); + + return { + theUserOwnsThisAnnotation, + getAnnotationIsLoading, + getAnnotationIsError, + hotColumnHeaders, + setAnnotationsHotState, + colWidths, + ...annotationsHotState, + }; +}; + +export default useEditAnnotationsHotTable; diff --git a/compose/neurosynth-frontend/src/components/HotTables/EditStudyAnnotationsHotTable/EditStudyAnnotationsHotTable.helpers.ts b/compose/neurosynth-frontend/src/components/HotTables/EditStudyAnnotationsHotTable/EditStudyAnnotationsHotTable.helpers.ts index 80b168479..e0f69f015 100644 --- a/compose/neurosynth-frontend/src/components/HotTables/EditStudyAnnotationsHotTable/EditStudyAnnotationsHotTable.helpers.ts +++ b/compose/neurosynth-frontend/src/components/HotTables/EditStudyAnnotationsHotTable/EditStudyAnnotationsHotTable.helpers.ts @@ -1,12 +1,12 @@ import { HotTableProps } from '@handsontable/react'; import { EPropertyType } from 'components/EditMetadata'; -import styles from 'components/HotTables/AnnotationsHotTable/AnnotationsHotTable.module.css'; import { EditStudyAnnotationsNoteCollectionReturn } from 'components/HotTables/EditStudyAnnotationsHotTable/EditStudyAnnotationsHotTable.types'; -import { NoteKeyType } from 'components/HotTables/helpers/utils'; -import { CellValue } from 'handsontable/common'; +import styles from 'components/HotTables/HotTables.module.css'; +import { NoteKeyType } from 'components/HotTables/HotTables.types'; import { ColumnSettings } from 'handsontable/settings'; import { numericValidator } from 'handsontable/validators'; import { IStoreAnalysis } from 'pages/Studies/StudyStore.helpers'; +import { booleanValidator } from 'components/HotTables/HotTables.utils'; export const HotSettings: HotTableProps = { licenseKey: 'non-commercial-and-evaluation', @@ -27,17 +27,6 @@ export const HotSettings: HotTableProps = { }, }; -export const booleanValidator = (value: CellValue, callback: (isValid: boolean) => void) => { - const isValid = - value === true || - value === false || - value === 'true' || - value === 'false' || - value === null || - value === ''; - callback(isValid); -}; - export const createStudyAnnotationColHeaders = (noteKeys: NoteKeyType[]): string[] => { return [ 'Analysis Name', @@ -46,10 +35,6 @@ export const createStudyAnnotationColHeaders = (noteKeys: NoteKeyType[]): string ]; }; -export const createStudyAnnotationColWidths = (noteKeys: NoteKeyType[]): number[] => { - return [200, 250, ...noteKeys.map((x) => 150)]; -}; - export const createStudyAnnotationColumns = (noteKeys: NoteKeyType[]) => [ { diff --git a/compose/neurosynth-frontend/src/components/HotTables/EditStudyAnnotationsHotTable/EditStudyAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/components/HotTables/EditStudyAnnotationsHotTable/EditStudyAnnotationsHotTable.tsx index 67030f1ce..9dbc374f5 100644 --- a/compose/neurosynth-frontend/src/components/HotTables/EditStudyAnnotationsHotTable/EditStudyAnnotationsHotTable.tsx +++ b/compose/neurosynth-frontend/src/components/HotTables/EditStudyAnnotationsHotTable/EditStudyAnnotationsHotTable.tsx @@ -4,7 +4,7 @@ import { HotSettings } from 'components/HotTables/EditStudyAnnotationsHotTable/E import { CellChange, ChangeSource, RangeType } from 'handsontable/common'; import { useRef } from 'react'; import { useAnnotationNoteKeys, useUpdateAnnotationNotes } from 'stores/AnnotationStore.actions'; -import { sanitizePaste } from '../helpers/utils'; +import { sanitizePaste } from '../HotTables.utils'; import useEditStudyAnnotationsHotTable from './useEditStudyAnnotationsHotTable'; const EditStudyAnnotationsHotTable: React.FC = (props) => { @@ -14,8 +14,6 @@ const EditStudyAnnotationsHotTable: React.FC = (props) => { const { colWidths, colHeaders, columns, hiddenRows, data, height } = useEditStudyAnnotationsHotTable(); - console.log('hello'); - const handleAfterChange = (changes: CellChange[] | null, source: ChangeSource) => { if (!data || !noteKeys || !changes) return; diff --git a/compose/neurosynth-frontend/src/components/HotTables/EditStudyAnnotationsHotTable/useEditStudyAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/components/HotTables/EditStudyAnnotationsHotTable/useEditStudyAnnotationsHotTable.tsx index bdba1475d..e9fc0cfb3 100644 --- a/compose/neurosynth-frontend/src/components/HotTables/EditStudyAnnotationsHotTable/useEditStudyAnnotationsHotTable.tsx +++ b/compose/neurosynth-frontend/src/components/HotTables/EditStudyAnnotationsHotTable/useEditStudyAnnotationsHotTable.tsx @@ -4,13 +4,13 @@ import { useAnnotationNoteKeys } from 'stores/AnnotationStore.actions'; import { useAnnotationNotes } from 'stores/AnnotationStore.getters'; import { createStudyAnnotationColHeaders, - createStudyAnnotationColWidths, createStudyAnnotationColumns, } from './EditStudyAnnotationsHotTable.helpers'; import { EditStudyAnnotationsNoteCollectionReturn, IEditStudyAnnotationsDataRef, } from './EditStudyAnnotationsHotTable.types'; +import { createColWidths } from 'components/HotTables/HotTables.utils'; const useEditStudyAnnotationsHotTable = () => { const studyId = useStudyId(); @@ -34,25 +34,39 @@ const useEditStudyAnnotationsHotTable = () => { }); }, [notes]); + /** + * this hook runs everytime (AND ONLY WHEN) the analyses change (i.e. when someone is updating the analysis name or description). + * From an annotation perspective, the analysis name and description is purely decorative so we debounce the updates here + */ useEffect(() => { - console.log(analyses); const timeout = setTimeout(() => { - const update: EditStudyAnnotationsNoteCollectionReturn[] = []; - analyses.forEach((analysis) => { - const foundNote = notes?.find((note) => note.analysis === analysis.id); - if (foundNote) - update.push({ - ...foundNote, + setData((prev) => { + if (!prev) return prev; + console.log({ + prev, + notes, + }); + const update: EditStudyAnnotationsNoteCollectionReturn[] = [...(notes || [])]; + analyses.forEach((analysis) => { + const foundNoteIndex = update.findIndex( + (updateNote) => updateNote.analysis === analysis.id + ); + if (foundNoteIndex < 0) return; + + update[foundNoteIndex] = { + ...update[foundNoteIndex], analysis_name: analysis.name || '', analysisDescription: analysis.description || '', - }); + }; + }); + return update; }); - setData(update); - }, 400); + }, 500); return () => { clearTimeout(timeout); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [analyses]); const hiddenRows = useMemo(() => { @@ -65,22 +79,10 @@ const useEditStudyAnnotationsHotTable = () => { return { columns: createStudyAnnotationColumns(noteKeys || []), colHeaders: createStudyAnnotationColHeaders(noteKeys || []), - colWidths: createStudyAnnotationColWidths(noteKeys || []), + colWidths: createColWidths(noteKeys || [], 200, 250, 150), }; }, [noteKeys]); - // const data = useMemo(() => { - // return (notes || []).map((note) => { - // const foundAnalysis = analyses.find((analysis) => analysis.id === note.analysis); - - // return { - // ...note, - // analysis_name: foundAnalysis ? foundAnalysis.name || '' : '', - // analysisDescription: foundAnalysis ? foundAnalysis.description || '' : '', - // }; - // }); - // }, [notes, analyses]); - const height = useMemo(() => { const MIN_HEIGHT_PX = 150; const MAX_HEIGHT_PX = 500; diff --git a/compose/neurosynth-frontend/src/components/HotTables/AnnotationsHotTable/AnnotationsHotTable.module.css b/compose/neurosynth-frontend/src/components/HotTables/HotTables.module.css similarity index 73% rename from compose/neurosynth-frontend/src/components/HotTables/AnnotationsHotTable/AnnotationsHotTable.module.css rename to compose/neurosynth-frontend/src/components/HotTables/HotTables.module.css index c75f2a131..37a9a005b 100644 --- a/compose/neurosynth-frontend/src/components/HotTables/AnnotationsHotTable/AnnotationsHotTable.module.css +++ b/compose/neurosynth-frontend/src/components/HotTables/HotTables.module.css @@ -3,6 +3,17 @@ color: black; } +.truncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.no-top-bottom-borders { + border-bottom-color: transparent !important; + border-top-color: transparent !important; +} + .no-wrap { /* max-width: 100px !important; */ overflow-wrap: break-word !important; diff --git a/compose/neurosynth-frontend/src/components/HotTables/HotTables.types.ts b/compose/neurosynth-frontend/src/components/HotTables/HotTables.types.ts new file mode 100644 index 000000000..cc75bda44 --- /dev/null +++ b/compose/neurosynth-frontend/src/components/HotTables/HotTables.types.ts @@ -0,0 +1,8 @@ +import { EPropertyType } from 'components/EditMetadata'; + +export interface NoteKeyType { + key: string; + type: EPropertyType; +} + +export type AnnotationNoteValue = string | number | boolean | null; diff --git a/compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.tsx b/compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.tsx new file mode 100644 index 000000000..5b9a092cb --- /dev/null +++ b/compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.tsx @@ -0,0 +1,72 @@ +import { EPropertyType } from 'components/EditMetadata'; +import { NoteKeyType } from 'components/HotTables/HotTables.types'; +import { CellValue } from 'handsontable/common'; + +export const noteKeyObjToArr = (noteKeys?: object | null): NoteKeyType[] => { + if (!noteKeys) return []; + const noteKeyTypes = noteKeys as { [key: string]: EPropertyType }; + const arr = Object.entries(noteKeyTypes).map(([key, type]) => ({ + key, + type, + })); + return arr; +}; + +export const noteKeyArrToObj = (noteKeyArr: NoteKeyType[]): { [key: string]: EPropertyType } => { + const noteKeyObj: { [key: string]: EPropertyType } = noteKeyArr.reduce((acc, curr) => { + acc[curr.key] = curr.type; + return acc; + }, {} as { [key: string]: EPropertyType }); + + return noteKeyObj; +}; + +export const booleanValidator = (value: CellValue, callback: (isValid: boolean) => void) => { + const isValid = + value === true || + value === false || + value === 'true' || + value === 'false' || + value === null || + value === ''; + callback(isValid); +}; + +export const replaceString = (val: string) => { + // replace = ['֊', '‐', '‑', '⁃', '﹣', '-', '‒', '–', '—', '﹘', '−', '-'] + + return val.replaceAll(new RegExp('֊|‐|‑|⁃|﹣|-|‒|–|—|﹘|−|-', 'g'), '-'); +}; + +export const stripTags = (stringWhichMayHaveHTML: any) => { + if (typeof stringWhichMayHaveHTML !== 'string') return ''; + + let doc = new DOMParser().parseFromString(stringWhichMayHaveHTML, 'text/html'); + return doc.body.textContent || ''; +}; + +export const sanitizePaste = (data: any[][]) => { + data.forEach((dataRow, rowIndex) => { + dataRow.forEach((value, valueIndex) => { + if (typeof value === 'number') return; + if (typeof value === 'boolean') return; + + let newVal = value; + newVal = stripTags(newVal); // strip all HTML tags that were copied over if they exist + newVal = replaceString(newVal); // replace minus operator with javascript character code + + if (newVal === 'true') newVal = true; + else if (newVal === 'false') newVal = false; + data[rowIndex][valueIndex] = newVal; + }); + }); +}; + +export const createColWidths = ( + noteKeys: NoteKeyType[], + first: number, + second: number, + colWidth?: number +): number[] => { + return [first, second, ...noteKeys.map((x) => (colWidth ? colWidth : 150))]; +}; diff --git a/compose/neurosynth-frontend/src/components/HotTables/StudyAnnotationsHotTable/StudyAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/components/HotTables/StudyAnnotationsHotTable/StudyAnnotationsHotTable.tsx deleted file mode 100644 index 87a24ac26..000000000 --- a/compose/neurosynth-frontend/src/components/HotTables/StudyAnnotationsHotTable/StudyAnnotationsHotTable.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import HotTable, { HotTableProps } from '@handsontable/react'; -import { Box } from '@mui/material'; -import styles from 'components/EditAnnotations/AnnotationsHotTable/AnnotationsHotTable.module.css'; -import { EPropertyType } from 'components/EditMetadata'; -import { CellCoords } from 'handsontable'; -import { CellChange, CellValue, ChangeSource } from 'handsontable/common'; -import { registerAllModules } from 'handsontable/registry'; -import { ColumnSettings } from 'handsontable/settings'; -import { numericValidator } from 'handsontable/validators'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { AnnotationNoteValue, NoteKeyType } from 'components/HotTables/helpers/utils'; - -const booleanValidator = (value: CellValue, callback: (isValid: boolean) => void) => { - const isValid = - value === true || - value === false || - value === 'true' || - value === 'false' || - value === null || - value === ''; - callback(isValid); -}; - -const createColumns = (noteKeys: NoteKeyType[]) => [ - { - className: `${styles['read-only-col']}`, - readOnly: true, - width: '200', - }, - { - className: styles['read-only-col'], - readOnly: true, - width: '150', - }, - ...noteKeys.map((x) => { - return { - readOnly: false, - className: styles[x.type], - allowInvalid: false, - type: x.type === EPropertyType.BOOLEAN ? 'checkbox' : 'text', - validator: - x.type === EPropertyType.NUMBER - ? numericValidator - : x.type === EPropertyType.BOOLEAN - ? booleanValidator - : undefined, - } as ColumnSettings; - }), -]; - -const hotSettings: HotTableProps = { - fillHandle: false, - licenseKey: 'non-commercial-and-evaluation', - contextMenu: false, - viewportRowRenderingOffset: 2, - viewportColumnRenderingOffset: 2, - width: '100%', - fixedColumnsStart: 2, -}; - -registerAllModules(); - -const StudyAnnotationsHotTable: React.FC<{ - onChange: (hotData: AnnotationNoteValue[][], updatedNoteKeys: NoteKeyType[]) => void; - hotData: AnnotationNoteValue[][]; - noteKeys: NoteKeyType[]; -}> = React.memo((props) => { - const hotTableRef = useRef(null); - const hotStateRef = useRef<{ - noteKeys: NoteKeyType[]; - }>({ - noteKeys: [], - }); - const { noteKeys: initialNoteKeys, hotData, onChange } = props; - - // set handsontable ref height if the (debouneced) window height changes. - // Must do this via an eventListener to avoid react re renders clearing the HOT State - useEffect(() => { - let timeout: any; - const handleResize = () => { - const currentWindowSize = window.innerHeight; - if (currentWindowSize) { - if (timeout) clearTimeout(timeout); - timeout = setTimeout(async () => { - if (hotTableRef.current?.hotInstance) { - const navHeight = '64px'; - const breadCrumbHeight = '44px'; - const addMetadataHeight = '1rem + 40px + 25px'; - const bottomSaveButtonHeight = '2.5rem'; - const pageMarginHeight = '4rem'; - hotTableRef.current.hotInstance.updateSettings({ - height: `calc(${currentWindowSize}px - ${navHeight} - ${breadCrumbHeight} - (${addMetadataHeight}) - ${bottomSaveButtonHeight} - ${pageMarginHeight})`, - }); - } - }, 200); - } - }; - window.addEventListener('resize', handleResize); - handleResize(); - - return () => { - if (timeout) clearTimeout(timeout); - window.removeEventListener('resize', handleResize); - }; - }, []); - - const [initialHotState, setInitialHotState] = useState<{ - initialHotData: AnnotationNoteValue[][]; - initialHotColumns: ColumnSettings[]; - intialHotColumnHeaders: string[]; - }>({ - initialHotData: [], - initialHotColumns: [], - intialHotColumnHeaders: [], - }); - - const handleRemoveHotColumn = useCallback( - (colKey: string) => { - if (!hotTableRef.current?.hotInstance) return; - - const foundIndex = hotStateRef.current.noteKeys.findIndex((x) => x.key === colKey); - if (foundIndex < 0) return; - - const noteKeys = hotStateRef.current.noteKeys; - const colHeaders = hotTableRef.current.hotInstance.getColHeader() as string[]; - const data = hotTableRef.current.hotInstance.getData() as AnnotationNoteValue[][]; - - noteKeys.splice(foundIndex, 1); - const columns = createColumns(noteKeys); - - colHeaders.splice(foundIndex + 2, 1); - data.forEach((row) => { - row.splice(foundIndex + 2, 1); - }); - - hotTableRef.current.hotInstance.updateSettings({ - data: data, - colHeaders: colHeaders, - columns: columns, - }); - - onChange( - hotTableRef.current.hotInstance.getData() as AnnotationNoteValue[][], - noteKeys - ); - }, - [onChange] - ); - - const handleCellMouseUp = ( - event: MouseEvent, - coords: CellCoords, - TD: HTMLTableCellElement - ): void => { - const target = event.target as HTMLButtonElement; - if (coords.row < 0 && (target.tagName === 'svg' || target.tagName === 'path')) { - handleRemoveHotColumn(TD.innerText); - } - }; - - const handleChangeOccurred = (changes: CellChange[] | null, source: ChangeSource) => { - // this hook is triggered during merge cells and on initial update. We don't want the parent to be notified unless its a real user change - if (!changes || changes.some((x) => x[1] === 0)) return; - if (hotTableRef.current?.hotInstance) { - const hotData = hotTableRef.current.hotInstance.getData() as AnnotationNoteValue[][]; - onChange(hotData, hotStateRef.current.noteKeys); - } - }; - - useEffect(() => { - setInitialHotState((state) => { - hotStateRef.current.noteKeys = JSON.parse(JSON.stringify(initialNoteKeys)); - - return { - initialHotData: JSON.parse(JSON.stringify(hotData)), - initialHotColumns: createColumns(initialNoteKeys), - intialHotColumnHeaders: [ - 'Analysis Name', - 'Description', - ...initialNoteKeys.map((col) => col.key), - ], - }; - }); - }, [hotData, initialNoteKeys, handleRemoveHotColumn]); - - return ( - - - - ); -}); - -export default StudyAnnotationsHotTable; diff --git a/compose/neurosynth-frontend/src/neurosynth-compose-typescript-sdk b/compose/neurosynth-frontend/src/neurosynth-compose-typescript-sdk index ddf6bc816..47eb6e2ac 160000 --- a/compose/neurosynth-frontend/src/neurosynth-compose-typescript-sdk +++ b/compose/neurosynth-frontend/src/neurosynth-compose-typescript-sdk @@ -1 +1 @@ -Subproject commit ddf6bc816fff82469eed171010f5698067165222 +Subproject commit 47eb6e2ac83e6a762e4a762c21666ab1234ec51e diff --git a/compose/neurosynth-frontend/src/pages/Annotations/AnnotationsPage/AnnotationsPage.tsx b/compose/neurosynth-frontend/src/pages/Annotations/AnnotationsPage/AnnotationsPage.tsx index c18747192..5211320f8 100644 --- a/compose/neurosynth-frontend/src/pages/Annotations/AnnotationsPage/AnnotationsPage.tsx +++ b/compose/neurosynth-frontend/src/pages/Annotations/AnnotationsPage/AnnotationsPage.tsx @@ -1,5 +1,6 @@ import { Box, Typography } from '@mui/material'; import EditAnnotations from 'components/EditAnnotations/EditAnnotations'; +import EditAnnotationsHotTable from 'components/HotTables/EditAnnotationsHotTable/EditAnnotationsHotTable'; import NeurosynthBreadcrumbs from 'components/NeurosynthBreadcrumbs/NeurosynthBreadcrumbs'; import StateHandlerComponent from 'components/StateHandlerComponent/StateHandlerComponent'; import { useGetAnnotationById } from 'hooks'; @@ -55,7 +56,9 @@ const AnnotationsPage: React.FC = () => { {data?.name} {data?.description || ''}
- + + +
); diff --git a/compose/neurosynth-frontend/src/pages/BaseNavigation/BaseNavigation.tsx b/compose/neurosynth-frontend/src/pages/BaseNavigation/BaseNavigation.tsx index 28741358e..ba24e78c9 100644 --- a/compose/neurosynth-frontend/src/pages/BaseNavigation/BaseNavigation.tsx +++ b/compose/neurosynth-frontend/src/pages/BaseNavigation/BaseNavigation.tsx @@ -83,16 +83,11 @@ const BaseNavigation: React.FC = (_props) => {
- + - - - - - { const { studyId } = useParams<{ studyId: string }>(); const queryClient = useQueryClient(); - const snackbar = useSnackbar(); + const { enqueueSnackbar } = useSnackbar(); const analyses = useStudyAnalyses(); const annotationId = useProjectExtractionAnnotationId(); @@ -55,6 +56,7 @@ const EditStudyPage: React.FC = (props) => { const clearStudyStore = useClearStudyStore(); const initStudyStore = useInitStudyStore(); // annotation stuff + const storeAnnotationId = useAnnotationId(); const clearAnnotationStore = useClearAnnotationStore(); const notes = useAnnotationNotes(); const initAnnotationStore = useInitAnnotationStore(); @@ -84,6 +86,8 @@ const EditStudyPage: React.FC = (props) => { await updateStudyInDB(annotationId as string); queryClient.invalidateQueries('studies'); queryClient.invalidateQueries('annotations'); + + enqueueSnackbar('Study saved', { variant: 'success' }); }; const handleUpdateBothInDB = async () => { @@ -107,11 +111,14 @@ const EditStudyPage: React.FC = (props) => { queryClient.invalidateQueries('studies'); queryClient.invalidateQueries('annotations'); + + enqueueSnackbar('Study and annotation saved', { variant: 'success' }); }; const handleUpdateAnnotationInDB = async () => { await updateAnnotationInDB(); queryClient.invalidateQueries('annotations'); + enqueueSnackbar('Annotation saved', { variant: 'success' }); }; const handleUpdateDB = () => { @@ -125,7 +132,7 @@ const EditStudyPage: React.FC = (props) => { } } catch (e) { console.error(e); - snackbar.enqueueSnackbar('There was an error saving to the database', { + enqueueSnackbar('There was an error saving to the database', { variant: 'error', }); } @@ -135,14 +142,14 @@ const EditStudyPage: React.FC = (props) => { const { isError: hasDuplicateError, errorMessage: hasDuplicateErrorMessage } = hasDuplicateStudyAnalysisNames(analyses); if (hasDuplicateError) { - snackbar.enqueueSnackbar(hasDuplicateErrorMessage, { variant: 'warning' }); + enqueueSnackbar(hasDuplicateErrorMessage, { variant: 'warning' }); return; } const { isError: emptyPointError, errorMessage: emptyPointErrorMessage } = hasEmptyStudyPoints(analyses); if (emptyPointError) { - snackbar.enqueueSnackbar(emptyPointErrorMessage, { variant: 'warning' }); + enqueueSnackbar(emptyPointErrorMessage, { variant: 'warning' }); return; } @@ -153,7 +160,7 @@ const EditStudyPage: React.FC = (props) => { diff --git a/compose/neurosynth-frontend/src/stores/AnnotationStore.getters.ts b/compose/neurosynth-frontend/src/stores/AnnotationStore.getters.ts index 19325a9ae..c3df2c6fe 100644 --- a/compose/neurosynth-frontend/src/stores/AnnotationStore.getters.ts +++ b/compose/neurosynth-frontend/src/stores/AnnotationStore.getters.ts @@ -10,3 +10,4 @@ export const useAnnotationIsEdited = () => export const useAnnotationIsError = () => useAnnotationStore((state) => state.storeMetadata.isError); +export const useAnnotationId = () => useAnnotationStore((state) => state.annotation.id); diff --git a/compose/neurosynth-frontend/src/stores/AnnotationStore.helpers.ts b/compose/neurosynth-frontend/src/stores/AnnotationStore.helpers.ts index a171b0bc0..213f13492 100644 --- a/compose/neurosynth-frontend/src/stores/AnnotationStore.helpers.ts +++ b/compose/neurosynth-frontend/src/stores/AnnotationStore.helpers.ts @@ -1,4 +1,4 @@ -import { NoteKeyType } from 'components/HotTables/helpers/utils'; +import { NoteKeyType } from 'components/HotTables/HotTables.types'; import { EPropertyType } from 'components/EditMetadata'; import { AnnotationNoteType } from 'stores/AnnotationStore.types'; diff --git a/compose/neurosynth-frontend/src/stores/AnnotationStore.types.ts b/compose/neurosynth-frontend/src/stores/AnnotationStore.types.ts index 644d2947b..9d75caa18 100644 --- a/compose/neurosynth-frontend/src/stores/AnnotationStore.types.ts +++ b/compose/neurosynth-frontend/src/stores/AnnotationStore.types.ts @@ -1,4 +1,4 @@ -import { NoteKeyType } from 'components/HotTables/helpers/utils'; +import { NoteKeyType } from 'components/HotTables/HotTables.types'; import { AnnotationReturnOneOf1, NoteCollectionReturn } from 'neurostore-typescript-sdk'; export type AnnotationStoreMetadata = { @@ -7,8 +7,6 @@ export type AnnotationStoreMetadata = { isError: boolean; // for http errors that occur }; -export type AnnotationNoteValue = string | number | boolean | null; - export interface IStoreNoteCollectionReturn extends NoteCollectionReturn { isNew?: boolean; }