From e181aa2dea7f31e2fb40d44eaaa3723b462791e0 Mon Sep 17 00:00:00 2001 From: Nicholas Lee Date: Thu, 30 Oct 2025 15:47:39 -0700 Subject: [PATCH 1/5] fix: added add metadata row feature in study annotations --- .../EditAnnotationsHotTable.helpers.tsx | 14 +-- .../hooks/useEditAnnotationsHotTable.tsx | 4 +- .../Study/components/EditStudyAnalysis.tsx | 36 +------ .../EditStudyAnalysisDeleteButton.tsx | 43 ++++++++ .../components/EditStudyAnalysisDetails.tsx | 34 +++--- .../EditStudyAnnotationsHotTable.helpers.ts | 8 -- .../EditStudyAnnotationsHotTable.tsx | 100 +++++++++++++----- .../useEditStudyAnnotationsHotTable.tsx | 88 --------------- .../hooks/useEditStudyAnnotationsHotTable.tsx | 52 +++------ .../src/pages/Study/store/StudyStore.ts | 1 + .../src/stores/AnnotationStore.actions.ts | 26 ++--- .../src/stores/AnnotationStore.ts | 59 ++++++----- .../src/stores/AnnotationStore.types.ts | 1 + 13 files changed, 205 insertions(+), 261 deletions(-) create mode 100644 compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnalysisDeleteButton.tsx delete mode 100644 compose/neurosynth-frontend/src/pages/Study/components/useEditStudyAnnotationsHotTable.tsx diff --git a/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.helpers.tsx b/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.helpers.tsx index 4824fed1a..aecbd5304 100644 --- a/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.helpers.tsx +++ b/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.helpers.tsx @@ -119,11 +119,7 @@ export const annotationNotesToHotData = ( }; }; -export const createColumnHeader = ( - colKey: string, - colType: EPropertyType, - allowRemoveColumn: boolean -) => { +export const createColumnHeader = (colKey: string, colType: EPropertyType, allowRemoveColumn: boolean) => { const allowRemove = allowRemoveColumn ? `
${renderToString( @@ -168,17 +164,15 @@ export const createColumns = (noteKeys: NoteKeyType[], disable?: boolean) => x.type === EPropertyType.NUMBER ? numericValidator : x.type === EPropertyType.BOOLEAN - ? booleanValidator - : undefined, + ? booleanValidator + : undefined, } as ColumnSettings; }), ] as ColumnSettings[]; // we can assume that the hashmap maintains order and is sorted by key // this function gets all merge cells and only merge cells. If a cell does not need to be merged, a mergeCellObj is not creatd -export const getMergeCells = ( - hotDataToStudyMapping: Map -) => { +export const getMergeCells = (hotDataToStudyMapping: Map) => { const mergeCells: MergeCellsSettings[] = []; let studyId: string; diff --git a/compose/neurosynth-frontend/src/pages/Annotations/hooks/useEditAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/pages/Annotations/hooks/useEditAnnotationsHotTable.tsx index a92711f07..40af62745 100644 --- a/compose/neurosynth-frontend/src/pages/Annotations/hooks/useEditAnnotationsHotTable.tsx +++ b/compose/neurosynth-frontend/src/pages/Annotations/hooks/useEditAnnotationsHotTable.tsx @@ -49,8 +49,8 @@ const useEditAnnotationsHotTable = (annotationId?: string, disableEdit?: boolean annotationNote.study_name && annotationNote.study_year ? `(${annotationNote.study_year}) ${annotationNote.study_name}` : annotationNote.study_name - ? annotationNote.study_name - : ''; + ? annotationNote.study_name + : ''; const analysisName = annotationNote.analysis_name || ''; diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnalysis.tsx b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnalysis.tsx index df6a9e875..dcf8d5199 100644 --- a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnalysis.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnalysis.tsx @@ -1,38 +1,20 @@ -import { Box, Button, Typography } from '@mui/material'; -import ConfirmationDialog from 'components/Dialogs/ConfirmationDialog'; -import { useDeleteAnalysis } from 'pages/Study/store/StudyStore'; -import { useState } from 'react'; -import { useDeleteAnnotationNote } from 'stores/AnnotationStore.actions'; +import { Box, Typography } from '@mui/material'; +import EditStudyAnalysisDetails from 'pages/Study/components/EditStudyAnalysisDetails'; import EditStudyAnalysisPoints from 'pages/Study/components/EditStudyAnalysisPoints'; import StudyAnalysisWarnings from 'pages/Study/components/StudyAnalysisWarnings'; -import EditStudyAnalysisDetails from 'pages/Study/components/EditStudyAnalysisDetails'; const EditStudyAnalysis: React.FC<{ analysisId?: string; onDeleteAnalysis: () => void; }> = (props) => { - const deleteAnalysis = useDeleteAnalysis(); - const deleteAnnotationNote = useDeleteAnnotationNote(); - - const [dialogIsOpen, setDialogIsOpen] = useState(false); - if (!props.analysisId) { return No analysis selected; } - const handleCloseDialog = (confirm?: boolean) => { - if (confirm && props.analysisId) { - deleteAnalysis(props.analysisId); - deleteAnnotationNote(props.analysisId); - props.onDeleteAnalysis(); - } - setDialogIsOpen(false); - }; - return ( - + {/* 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 */} {/* @@ -41,18 +23,6 @@ const EditStudyAnalysis: React.FC<{ */} - - - - ); }; diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnalysisDeleteButton.tsx b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnalysisDeleteButton.tsx new file mode 100644 index 000000000..f0e1a1db5 --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnalysisDeleteButton.tsx @@ -0,0 +1,43 @@ +import { Button, ButtonOwnProps } from '@mui/material'; +import ConfirmationDialog from 'components/Dialogs/ConfirmationDialog'; +import { useDeleteAnalysis } from 'pages/Study/store/StudyStore'; +import { useState } from 'react'; +import { useDeleteAnnotationNote } from 'stores/AnnotationStore.actions'; + +const EditStudyAnalysisDeleteButton: React.FC< + ButtonOwnProps & { + analysisId?: string; + onDeleteAnalysis: () => void; + } +> = ({ analysisId, onDeleteAnalysis, ...buttonProps }) => { + const deleteAnalysis = useDeleteAnalysis(); + const deleteAnnotationNote = useDeleteAnnotationNote(); + + const [dialogIsOpen, setDialogIsOpen] = useState(false); + + const handleCloseDialog = (confirm?: boolean) => { + if (confirm && analysisId) { + deleteAnalysis(analysisId); + deleteAnnotationNote(analysisId); + onDeleteAnalysis(); + } + setDialogIsOpen(false); + }; + + return ( + <> + + + + ); +}; + +export default EditStudyAnalysisDeleteButton; diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnalysisDetails.tsx b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnalysisDetails.tsx index 68055e2c7..c62b46fd2 100644 --- a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnalysisDetails.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnalysisDetails.tsx @@ -7,8 +7,9 @@ import { import { IStoreAnalysis } from 'pages/Study/store/StudyStore.helpers'; import { useEffect } from 'react'; import { useUpdateAnnotationNoteName } from 'stores/AnnotationStore.actions'; +import EditStudyAnalysisDeleteButton from './EditStudyAnalysisDeleteButton'; -const EditStudyAnalysisDetails: React.FC<{ analysisId?: string }> = (props) => { +const EditStudyAnalysisDetails: React.FC<{ analysisId?: string; onDeleteAnalysis: () => void }> = (props) => { const addOrUpdateAnalysis = useAddOrUpdateAnalysis(); const name = useStudyAnalysisName(props.analysisId); const description = useStudyAnalysisDescription(props.analysisId); @@ -16,8 +17,7 @@ const EditStudyAnalysisDetails: React.FC<{ analysisId?: string }> = (props) => { useEffect(() => { if (!props.analysisId) return; - let debounce: NodeJS.Timeout; - debounce = setTimeout(() => { + const debounce: NodeJS.Timeout = setTimeout(() => { updateAnnotationNoteName({ analysis: props.analysisId, analysis_name: name, @@ -29,11 +29,7 @@ const EditStudyAnalysisDetails: React.FC<{ analysisId?: string }> = (props) => { }; }, [name, props.analysisId, updateAnnotationNoteName]); - const handleUpdateAnalysisDetails = ( - field: keyof IStoreAnalysis, - analysisId: string, - value: string - ) => { + const handleUpdateAnalysisDetails = (field: keyof IStoreAnalysis, analysisId: string, value: string) => { if (!analysisId) return; addOrUpdateAnalysis({ id: analysisId, @@ -43,9 +39,19 @@ const EditStudyAnalysisDetails: React.FC<{ analysisId?: string }> = (props) => { return ( - - Analysis Details - + + Analysis Details + + Delete Analysis + + = (props) => { /> { - handleUpdateAnalysisDetails( - 'description', - props.analysisId || '', - event.target.value - ); + handleUpdateAnalysisDetails('description', props.analysisId || '', event.target.value); }} label="description" size="small" diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.helpers.ts b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.helpers.ts index 8c74ea646..151261bd0 100644 --- a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.helpers.ts +++ b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.helpers.ts @@ -27,14 +27,6 @@ export const HotSettings: HotTableProps = { }, }; -export const createStudyAnnotationColHeaders = (noteKeys: NoteKeyType[]): string[] => { - return [ - 'Analysis Name', - 'Analysis Description', - ...noteKeys.map((x) => `
${x.key}
`), - ]; -}; - export const createStudyAnnotationColumns = (noteKeys: NoteKeyType[], readonly: boolean) => [ { diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.tsx index e16cf7f61..59dcac4eb 100644 --- a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.tsx @@ -1,19 +1,30 @@ import { HotTable } from '@handsontable/react'; import { Box } from '@mui/material'; -import { CellChange, ChangeSource, RangeType } from 'handsontable/common'; -import { useMemo, useRef } from 'react'; -import { useAnnotationNoteKeys, useUpdateAnnotationNotes } from 'stores/AnnotationStore.actions'; +import AddMetadataRow from 'components/EditMetadata/AddMetadataRow'; +import { getType, IMetadataRowModel } from 'components/EditMetadata/EditMetadata.types'; import { sanitizePaste } from 'components/HotTables/HotTables.utils'; -import useEditStudyAnnotationsHotTable from 'pages/Study/components/useEditStudyAnnotationsHotTable'; +import { CellChange } from 'handsontable/common'; +import { useUserCanEdit } from 'hooks'; +import { useProjectUser } from 'pages/Project/store/ProjectStore'; import { HotSettings } from 'pages/Study/components/EditStudyAnnotationsHotTable.helpers'; +import useEditStudyAnnotationsHotTable from 'pages/Study/hooks/useEditStudyAnnotationsHotTable'; +import { useMemo, useRef } from 'react'; +import { + useAnnotationNoteKeys, + useCreateAnnotationColumn, + useUpdateAnnotationNotes, +} from 'stores/AnnotationStore.actions'; const EditStudyAnnotationsHotTable: React.FC<{ readonly?: boolean }> = ({ readonly = false }) => { const hotTableRef = useRef(null); const noteKeys = useAnnotationNoteKeys(); const updateNotes = useUpdateAnnotationNotes(); - const { colWidths, colHeaders, columns, hiddenRows, data } = useEditStudyAnnotationsHotTable(hotTableRef, readonly); + const createAnnotationColumn = useCreateAnnotationColumn(); + const { colWidths, colHeaders, columns, hiddenRows, data } = useEditStudyAnnotationsHotTable(readonly); + const projectUser = useProjectUser(); + const canEdit = useUserCanEdit(projectUser || undefined); - const handleAfterChange = (changes: CellChange[] | null, source: ChangeSource) => { + const handleAfterChange = (changes: CellChange[] | null) => { if (!data || !noteKeys || !changes) return; const updatedNotes = [...data]; @@ -37,35 +48,74 @@ const EditStudyAnnotationsHotTable: React.FC<{ readonly?: boolean }> = ({ readon updateNotes(updatedNotes); }; - const handleBeforePaste = (pastedData: any[][], coords: RangeType[]) => { + const handleBeforePaste = (pastedData: unknown[][]) => { if (!data) return false; sanitizePaste(pastedData); return true; }; + const handleAddHotColumn = (row: IMetadataRowModel) => { + if (!noteKeys) return false; + const trimmedKey = row.metadataKey.trim(); + if (noteKeys.find((x) => x.key === trimmedKey)) return false; + + createAnnotationColumn({ + key: trimmedKey, + type: getType(row.metadataValue), + }); + + return true; + }; + const memoizedData = useMemo(() => { return JSON.parse(JSON.stringify(data || [])); }, [data]); return ( - - + + {canEdit && !readonly && ( + + + + )} + + + ); }; diff --git a/compose/neurosynth-frontend/src/pages/Study/components/useEditStudyAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/pages/Study/components/useEditStudyAnnotationsHotTable.tsx deleted file mode 100644 index 01b467de9..000000000 --- a/compose/neurosynth-frontend/src/pages/Study/components/useEditStudyAnnotationsHotTable.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { createColWidths } from 'components/HotTables/HotTables.utils'; -import { useDebouncedStudyAnalyses, useStudyId } from 'pages/Study/store/StudyStore'; -import { useEffect, useMemo, useState } from 'react'; -import { useAnnotationNoteKeys } from 'stores/AnnotationStore.actions'; -import { useAnnotationNotes } from 'stores/AnnotationStore.getters'; -import { createStudyAnnotationColHeaders, createStudyAnnotationColumns } from './EditStudyAnnotationsHotTable.helpers'; -import { - EditStudyAnnotationsNoteCollectionReturn, - IEditStudyAnnotationsDataRef, -} from './EditStudyAnnotationsHotTable.types'; -import { IStoreNoteCollectionReturn } from 'stores/AnnotationStore.types'; -import { IStoreAnalysis } from '../store/StudyStore.helpers'; -import HotTable from '@handsontable/react/hotTable'; -import useGetWindowWidth from 'hooks/useGetWindowWidth'; - -const useEditStudyAnnotationsHotTable = (hotTableRef: React.RefObject, readonly?: boolean) => { - const studyId = useStudyId(); - const debouncedAnalyses = useDebouncedStudyAnalyses(); - const noteKeys = useAnnotationNoteKeys(); - const notes = useAnnotationNotes(); - - const [data, setData] = useState(); - - useEffect(() => { - if (!notes) return; - setData((prev) => { - if (!prev) return [...notes]; - const update: { note: IStoreNoteCollectionReturn; analysis?: IStoreAnalysis }[] = notes.map((note) => { - const analysisId = note.analysis; - if (!analysisId) return { note, analysis: undefined }; - const foundIndex = debouncedAnalyses.findIndex( - (debouncedAnalysis) => debouncedAnalysis.id === analysisId - ); - if (foundIndex < 0) return { note, analysis: undefined }; - - const updatedNote = { - ...note, - analysis_name: debouncedAnalyses[foundIndex].name, - analysisDescription: debouncedAnalyses[foundIndex].description, - }; - - return { note: updatedNote, analysis: debouncedAnalyses[foundIndex] }; - }); - - return update - .sort((a, b) => { - if (!a.analysis?.order || !b.analysis?.order) return 0; - return ( - new Date(a.analysis.created_at || '').valueOf() - - new Date(b.analysis.created_at || '').valueOf() - ); - }) - .map((x) => x.note); - }); - }, [debouncedAnalyses, notes]); - - const hiddenRows = useMemo(() => { - return (notes || []) - .map((x, index) => (x.study !== studyId ? index : null)) - .filter((y) => y !== null) as number[]; - }, [notes, studyId]); - - const { columns, colHeaders, colWidths } = useMemo(() => { - return { - columns: createStudyAnnotationColumns(noteKeys || [], !!readonly), - colHeaders: createStudyAnnotationColHeaders(noteKeys || []), - colWidths: createColWidths(noteKeys || [], 200, 200, 150), - }; - }, [noteKeys, readonly]); - - const windowWidth = useGetWindowWidth(); - useEffect(() => { - if (windowWidth && hotTableRef.current?.hotInstance) { - hotTableRef.current.hotInstance.refreshDimensions(); - hotTableRef.current.hotInstance.render(); - } - }, [windowWidth, hotTableRef]); - - return { - hiddenRows, - columns, - colHeaders, - colWidths, - data: data || [], - }; -}; - -export default useEditStudyAnnotationsHotTable; diff --git a/compose/neurosynth-frontend/src/pages/Study/hooks/useEditStudyAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/pages/Study/hooks/useEditStudyAnnotationsHotTable.tsx index cc35b64dc..7b54f52c9 100644 --- a/compose/neurosynth-frontend/src/pages/Study/hooks/useEditStudyAnnotationsHotTable.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/hooks/useEditStudyAnnotationsHotTable.tsx @@ -3,14 +3,12 @@ import { useDebouncedStudyAnalyses, useStudyId } from 'pages/Study/store/StudySt import { useEffect, useMemo, useState } from 'react'; import { useAnnotationNoteKeys } from 'stores/AnnotationStore.actions'; import { useAnnotationNotes } from 'stores/AnnotationStore.getters'; -import { - createStudyAnnotationColHeaders, - createStudyAnnotationColumns, -} from 'pages/Study/components/EditStudyAnnotationsHotTable.helpers'; +import { createStudyAnnotationColumns } from '../components/EditStudyAnnotationsHotTable.helpers'; import { EditStudyAnnotationsNoteCollectionReturn, IEditStudyAnnotationsDataRef, -} from 'pages/Study/components/EditStudyAnnotationsHotTable.types'; +} from '../components/EditStudyAnnotationsHotTable.types'; +import { createColumnHeader } from 'pages/Annotations/components/EditAnnotationsHotTable.helpers'; const useEditStudyAnnotationsHotTable = (readonly?: boolean) => { const studyId = useStudyId(); @@ -22,16 +20,7 @@ const useEditStudyAnnotationsHotTable = (readonly?: boolean) => { useEffect(() => { if (!notes) return; - - setData((prev) => { - if (!prev) return [...notes]; - - const update = [...prev].map((updateItem, index) => ({ - ...updateItem, - ...notes[index], - })); - return update; - }); + setData([...notes]); }, [notes]); /** @@ -41,11 +30,9 @@ const useEditStudyAnnotationsHotTable = (readonly?: boolean) => { useEffect(() => { setData((prev) => { if (!prev) return prev; - const update: EditStudyAnnotationsNoteCollectionReturn[] = [...(notes || [])]; + const update = [...prev]; debouncedAnalyses.forEach((analysis) => { - const foundNoteIndex = update.findIndex( - (updateNote) => updateNote.analysis === analysis.id - ); + const foundNoteIndex = update.findIndex((updateNote) => updateNote.analysis === analysis.id); if (foundNoteIndex < 0) return; update[foundNoteIndex] = { @@ -56,9 +43,9 @@ const useEditStudyAnnotationsHotTable = (readonly?: boolean) => { }); return update; }); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [debouncedAnalyses]); + // only show the indices for rows that we want to hide const hiddenRows = useMemo(() => { return (notes || []) .map((x, index) => (x.study !== studyId ? index : null)) @@ -68,34 +55,21 @@ const useEditStudyAnnotationsHotTable = (readonly?: boolean) => { const { columns, colHeaders, colWidths } = useMemo(() => { return { columns: createStudyAnnotationColumns(noteKeys || [], !!readonly), - colHeaders: createStudyAnnotationColHeaders(noteKeys || []), - colWidths: createColWidths(noteKeys || [], 200, 250, 150), + colHeaders: [ + 'Analysis Name', + 'Analysis Description', + ...(noteKeys ?? []).map((x) => createColumnHeader(x.key, x.type, false)), + ], + colWidths: createColWidths(noteKeys || [], 150, 150, 150), }; }, [noteKeys, readonly]); - const height = useMemo(() => { - const MIN_HEIGHT_PX = 100; - const MAX_HEIGHT_PX = 500; - const HEADER_HEIGHT_PX = 26; - const ROW_HEIGHT_PX = 24; // +24 to padd row height a little bit - - const visibleNotes = (notes || []).filter((x) => x.study === studyId); - - const TABLE_HEIGHT_PX = HEADER_HEIGHT_PX + visibleNotes.length * ROW_HEIGHT_PX; - return TABLE_HEIGHT_PX < MIN_HEIGHT_PX - ? MIN_HEIGHT_PX - : TABLE_HEIGHT_PX > MAX_HEIGHT_PX - ? MAX_HEIGHT_PX - : TABLE_HEIGHT_PX; - }, [notes, studyId]); - return { hiddenRows, columns, colHeaders, colWidths, data: data || [], - height, }; }; diff --git a/compose/neurosynth-frontend/src/pages/Study/store/StudyStore.ts b/compose/neurosynth-frontend/src/pages/Study/store/StudyStore.ts index f8493f0f5..b95255f91 100644 --- a/compose/neurosynth-frontend/src/pages/Study/store/StudyStore.ts +++ b/compose/neurosynth-frontend/src/pages/Study/store/StudyStore.ts @@ -657,6 +657,7 @@ export const useDebouncedStudyAnalyses = () => { const unsub = useStudyStore.subscribe((state) => { if (debounce) clearTimeout(debounce); debounce = setTimeout(() => { + console.log({ state }); setDebouncedAnalyses(state.study.analyses); }, 400); }); diff --git a/compose/neurosynth-frontend/src/stores/AnnotationStore.actions.ts b/compose/neurosynth-frontend/src/stores/AnnotationStore.actions.ts index 9abb97029..82059c745 100644 --- a/compose/neurosynth-frontend/src/stores/AnnotationStore.actions.ts +++ b/compose/neurosynth-frontend/src/stores/AnnotationStore.actions.ts @@ -1,26 +1,20 @@ import { useAnnotationStore } from './AnnotationStore'; -export const useInitAnnotationStore = () => - useAnnotationStore((state) => state.initAnnotationStore); +export const useInitAnnotationStore = () => useAnnotationStore((state) => state.initAnnotationStore); -export const useSetAnnotationIsEdited = () => - useAnnotationStore((state) => state.setAnnotationIsEdited); +export const useSetAnnotationIsEdited = () => useAnnotationStore((state) => state.setAnnotationIsEdited); -export const useClearAnnotationStore = () => - useAnnotationStore((state) => state.clearAnnotationStore); +export const useClearAnnotationStore = () => useAnnotationStore((state) => state.clearAnnotationStore); -export const useAnnotationNoteKeys = () => - useAnnotationStore((state) => state.annotation.note_keys); +export const useAnnotationNoteKeys = () => useAnnotationStore((state) => state.annotation.note_keys); export const useUpdateAnnotationNotes = () => useAnnotationStore((state) => state.updateNotes); -export const useUpdateAnnotationInDB = () => - useAnnotationStore((state) => state.updateAnnotationInDB); +export const useCreateAnnotationColumn = () => useAnnotationStore((state) => state.createAnnotationColumn); -export const useCreateAnnotationNote = () => - useAnnotationStore((state) => state.createAnnotationNote); +export const useUpdateAnnotationInDB = () => useAnnotationStore((state) => state.updateAnnotationInDB); -export const useDeleteAnnotationNote = () => - useAnnotationStore((state) => state.deleteAnnotationNote); -export const useUpdateAnnotationNoteName = () => - useAnnotationStore((state) => state.updateAnnotationNoteName); +export const useCreateAnnotationNote = () => useAnnotationStore((state) => state.createAnnotationNote); + +export const useDeleteAnnotationNote = () => useAnnotationStore((state) => state.deleteAnnotationNote); +export const useUpdateAnnotationNoteName = () => useAnnotationStore((state) => state.updateAnnotationNoteName); diff --git a/compose/neurosynth-frontend/src/stores/AnnotationStore.ts b/compose/neurosynth-frontend/src/stores/AnnotationStore.ts index 723bccb15..2e8c0f98b 100644 --- a/compose/neurosynth-frontend/src/stores/AnnotationStore.ts +++ b/compose/neurosynth-frontend/src/stores/AnnotationStore.ts @@ -14,6 +14,7 @@ import { IStoreNoteCollectionReturn, } from 'stores/AnnotationStore.types'; import { setUnloadHandler } from 'helpers/BeforeUnload.helpers'; +import { noteKeyArrToObj } from 'components/HotTables/HotTables.utils'; export const useAnnotationStore = create< { @@ -58,18 +59,13 @@ export const useAnnotationStore = create< })); try { - const annotationRes = ( - await API.NeurostoreServices.AnnotationsService.annotationsIdGet(annotationId) - ).data as AnnotationReturnOneOf1; + const annotationRes = (await API.NeurostoreServices.AnnotationsService.annotationsIdGet(annotationId)) + .data as AnnotationReturnOneOf1; const noteKeysArr = noteKeyObjToArr(annotationRes.note_keys); - const notes: IStoreNoteCollectionReturn[] = ( - annotationRes.notes as Array - ) - ?.map((x) => ({ ...x, isNew: false })) - ?.sort((a, b) => - (a?.analysis_name || '').localeCompare(b?.analysis_name || '') - ); + const notes: IStoreNoteCollectionReturn[] = (annotationRes.notes as Array)?.map( + (x) => ({ ...x, isNew: false }) + ); set((state) => ({ ...state, @@ -77,7 +73,7 @@ export const useAnnotationStore = create< ...state.annotation, ...annotationRes, notes: notes, - note_keys: [...noteKeysArr], + note_keys: noteKeysArr, }, storeMetadata: { ...state.storeMetadata, @@ -151,6 +147,27 @@ export const useAnnotationStore = create< }, })); }, + createAnnotationColumn: (noteKey) => { + setUnloadHandler('annotation'); + set((state) => ({ + ...state, + annotation: { + ...state.annotation, + note_keys: [{ ...noteKey }, ...(state.annotation.note_keys ?? [])], + notes: (state.annotation.notes ?? []).map((note) => ({ + ...note, + note: { + ...note.note, + [noteKey.key]: null, + }, + })), + }, + storeMetadata: { + ...state.storeMetadata, + annotationIsEdited: true, + }, + })); + }, updateAnnotationNoteName: (note) => { set((state) => ({ ...state, @@ -217,22 +234,16 @@ export const useAnnotationStore = create< })); const annotationRes = ( - await API.NeurostoreServices.AnnotationsService.annotationsIdPut( - state.annotation.id, - { - notes: storeNotesToDBNotes(state.annotation.notes), - } - ) + await API.NeurostoreServices.AnnotationsService.annotationsIdPut(state.annotation.id, { + note_keys: noteKeyArrToObj(state.annotation.note_keys ?? []), + notes: storeNotesToDBNotes(state.annotation.notes), + }) ).data as AnnotationReturnOneOf1; const noteKeysArr = noteKeyObjToArr(annotationRes.note_keys); - const notes: IStoreNoteCollectionReturn[] = ( - annotationRes.notes as Array - ) - ?.map((x) => ({ ...x, isNew: false })) - ?.sort((a, b) => - (a?.analysis_name || '').localeCompare(b?.analysis_name || '') - ); + const notes: IStoreNoteCollectionReturn[] = (annotationRes.notes as Array)?.map( + (x) => ({ ...x, isNew: false }) + ); set((state) => ({ ...state, diff --git a/compose/neurosynth-frontend/src/stores/AnnotationStore.types.ts b/compose/neurosynth-frontend/src/stores/AnnotationStore.types.ts index bf870671b..59323c30b 100644 --- a/compose/neurosynth-frontend/src/stores/AnnotationStore.types.ts +++ b/compose/neurosynth-frontend/src/stores/AnnotationStore.types.ts @@ -22,6 +22,7 @@ export type AnnotationStoreActions = { setAnnotationIsEdited: (isEdited: boolean) => void; clearAnnotationStore: () => void; updateNotes: (updatedNotes: Array) => void; + createAnnotationColumn: (noteKey: NoteKeyType) => void; updateAnnotationInDB: () => Promise; createAnnotationNote: (analysisId: string, studyId: string, analysisName: string) => void; deleteAnnotationNote: (analysisId: string) => void; From e3539dd9a6c737889b61c073106d941099f60f9d Mon Sep 17 00:00:00 2001 From: Nicholas Lee Date: Fri, 7 Nov 2025 17:12:53 -0800 Subject: [PATCH 2/5] feat: add edit study annotation numeric and column deletion support --- .../EditAnnotationsHotTable.helpers.tsx | 2 +- .../components/EditAnnotationsHotTable.tsx | 1 - .../hooks/useEditAnnotationsHotTable.tsx | 2 +- .../EditStudyAnnotationsHotTable.tsx | 40 ++++++++++++++++++- .../hooks/useEditStudyAnnotationsHotTable.tsx | 2 +- .../src/pages/Study/store/StudyStore.ts | 1 - .../src/stores/AnnotationStore.actions.ts | 2 + .../src/stores/AnnotationStore.ts | 26 ++++++++++++ .../src/stores/AnnotationStore.types.ts | 1 + 9 files changed, 70 insertions(+), 7 deletions(-) diff --git a/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.helpers.tsx b/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.helpers.tsx index aecbd5304..6a6471297 100644 --- a/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.helpers.tsx +++ b/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.helpers.tsx @@ -137,7 +137,7 @@ export const createColumnHeader = (colKey: string, colType: EPropertyType, allow return ( `
` + - `
${colKey}
` + + `
${colKey}
` + allowRemove + `
` ); diff --git a/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.tsx index 4ba664e50..edd26110a 100644 --- a/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.tsx +++ b/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.tsx @@ -225,7 +225,6 @@ const AnnotationsHotTable: React.FC<{ annotationId?: string }> = React.memo((pro keyPlaceholderText="New Annotation Key" onAddMetadataRow={handleAddHotColumn} showMetadataValueInput={false} - allowNumber={false} allowNone={false} errorMessage="can't add column (key already exists)" /> diff --git a/compose/neurosynth-frontend/src/pages/Annotations/hooks/useEditAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/pages/Annotations/hooks/useEditAnnotationsHotTable.tsx index 40af62745..9b87234f3 100644 --- a/compose/neurosynth-frontend/src/pages/Annotations/hooks/useEditAnnotationsHotTable.tsx +++ b/compose/neurosynth-frontend/src/pages/Annotations/hooks/useEditAnnotationsHotTable.tsx @@ -83,7 +83,7 @@ const useEditAnnotationsHotTable = (annotationId?: string, disableEdit?: boolean }, [annotationsHotState.noteKeys]); const colWidths = useMemo(() => { - return createColWidths(annotationsHotState.noteKeys, 300, 150, 200); + return createColWidths(annotationsHotState.noteKeys, 300, 150, 150); }, [annotationsHotState.noteKeys]); const rowHeights = useMemo(() => { diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.tsx index 59dcac4eb..cfb33285f 100644 --- a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.tsx @@ -1,17 +1,20 @@ import { HotTable } from '@handsontable/react'; import { Box } from '@mui/material'; +import ConfirmationDialog from 'components/Dialogs/ConfirmationDialog'; import AddMetadataRow from 'components/EditMetadata/AddMetadataRow'; import { getType, IMetadataRowModel } from 'components/EditMetadata/EditMetadata.types'; import { sanitizePaste } from 'components/HotTables/HotTables.utils'; +import CellCoords from 'handsontable/3rdparty/walkontable/src/cell/coords'; import { CellChange } from 'handsontable/common'; import { useUserCanEdit } from 'hooks'; import { useProjectUser } from 'pages/Project/store/ProjectStore'; import { HotSettings } from 'pages/Study/components/EditStudyAnnotationsHotTable.helpers'; import useEditStudyAnnotationsHotTable from 'pages/Study/hooks/useEditStudyAnnotationsHotTable'; -import { useMemo, useRef } from 'react'; +import { useMemo, useRef, useState } from 'react'; import { useAnnotationNoteKeys, useCreateAnnotationColumn, + useRemoveAnnotationColumn, useUpdateAnnotationNotes, } from 'stores/AnnotationStore.actions'; @@ -20,9 +23,14 @@ const EditStudyAnnotationsHotTable: React.FC<{ readonly?: boolean }> = ({ readon const noteKeys = useAnnotationNoteKeys(); const updateNotes = useUpdateAnnotationNotes(); const createAnnotationColumn = useCreateAnnotationColumn(); + const removeAnnotationColumn = useRemoveAnnotationColumn(); const { colWidths, colHeaders, columns, hiddenRows, data } = useEditStudyAnnotationsHotTable(readonly); const projectUser = useProjectUser(); const canEdit = useUserCanEdit(projectUser || undefined); + const [confirmationDialogState, setConfirmationDialogState] = useState({ + isOpen: false, + colKey: '', + }); const handleAfterChange = (changes: CellChange[] | null) => { if (!data || !noteKeys || !changes) return; @@ -67,6 +75,26 @@ const EditStudyAnnotationsHotTable: React.FC<{ readonly?: boolean }> = ({ readon return true; }; + const handleConfirmationDialogClose = (confirm: boolean | undefined) => { + if (confirm) { + removeAnnotationColumn(confirmationDialogState.colKey); + } + setConfirmationDialogState({ + isOpen: false, + colKey: '', + }); + }; + + const handleCellMouseUp = (event: MouseEvent, coords: CellCoords, TD: HTMLTableCellElement) => { + const target = event.target as HTMLButtonElement; + if (coords.row < 0 && (target.tagName === 'svg' || target.tagName === 'path')) { + setConfirmationDialogState({ + isOpen: true, + colKey: TD.innerText, + }); + } + }; + const memoizedData = useMemo(() => { return JSON.parse(JSON.stringify(data || [])); }, [data]); @@ -91,13 +119,20 @@ const EditStudyAnnotationsHotTable: React.FC<{ readonly?: boolean }> = ({ readon keyPlaceholderText="New Column" onAddMetadataRow={handleAddHotColumn} showMetadataValueInput={false} - allowNumber={false} allowNone={false} errorMessage="can't add column (key already exists)" />
)} + = ({ readon rows: hiddenRows, indicators: false, }} + afterOnCellMouseUp={handleCellMouseUp} colWidths={colWidths} columns={columns} colHeaders={colHeaders} diff --git a/compose/neurosynth-frontend/src/pages/Study/hooks/useEditStudyAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/pages/Study/hooks/useEditStudyAnnotationsHotTable.tsx index 7b54f52c9..13a22b784 100644 --- a/compose/neurosynth-frontend/src/pages/Study/hooks/useEditStudyAnnotationsHotTable.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/hooks/useEditStudyAnnotationsHotTable.tsx @@ -58,7 +58,7 @@ const useEditStudyAnnotationsHotTable = (readonly?: boolean) => { colHeaders: [ 'Analysis Name', 'Analysis Description', - ...(noteKeys ?? []).map((x) => createColumnHeader(x.key, x.type, false)), + ...(noteKeys ?? []).map((x) => createColumnHeader(x.key, x.type, true)), ], colWidths: createColWidths(noteKeys || [], 150, 150, 150), }; diff --git a/compose/neurosynth-frontend/src/pages/Study/store/StudyStore.ts b/compose/neurosynth-frontend/src/pages/Study/store/StudyStore.ts index b95255f91..f8493f0f5 100644 --- a/compose/neurosynth-frontend/src/pages/Study/store/StudyStore.ts +++ b/compose/neurosynth-frontend/src/pages/Study/store/StudyStore.ts @@ -657,7 +657,6 @@ export const useDebouncedStudyAnalyses = () => { const unsub = useStudyStore.subscribe((state) => { if (debounce) clearTimeout(debounce); debounce = setTimeout(() => { - console.log({ state }); setDebouncedAnalyses(state.study.analyses); }, 400); }); diff --git a/compose/neurosynth-frontend/src/stores/AnnotationStore.actions.ts b/compose/neurosynth-frontend/src/stores/AnnotationStore.actions.ts index 82059c745..fdc826619 100644 --- a/compose/neurosynth-frontend/src/stores/AnnotationStore.actions.ts +++ b/compose/neurosynth-frontend/src/stores/AnnotationStore.actions.ts @@ -12,6 +12,8 @@ export const useUpdateAnnotationNotes = () => useAnnotationStore((state) => stat export const useCreateAnnotationColumn = () => useAnnotationStore((state) => state.createAnnotationColumn); +export const useRemoveAnnotationColumn = () => useAnnotationStore((state) => state.removeAnnotationColumn); + export const useUpdateAnnotationInDB = () => useAnnotationStore((state) => state.updateAnnotationInDB); export const useCreateAnnotationNote = () => useAnnotationStore((state) => state.createAnnotationNote); diff --git a/compose/neurosynth-frontend/src/stores/AnnotationStore.ts b/compose/neurosynth-frontend/src/stores/AnnotationStore.ts index 2e8c0f98b..d5ef7ebde 100644 --- a/compose/neurosynth-frontend/src/stores/AnnotationStore.ts +++ b/compose/neurosynth-frontend/src/stores/AnnotationStore.ts @@ -168,6 +168,32 @@ export const useAnnotationStore = create< }, })); }, + removeAnnotationColumn: (noteKey) => { + setUnloadHandler('annotation'); + set((state) => { + if (!state.annotation.note_keys || !state.annotation.notes) return state; + const updatedNoteKeys = state.annotation.note_keys.filter((x) => x.key !== noteKey); + const updatedNotes = [...state.annotation.notes]; + updatedNotes.forEach((note) => { + const typedNote = note.note as Record | undefined; + if (!typedNote) return; + delete typedNote[noteKey]; + }); + + return { + ...state, + annotation: { + ...state.annotation, + note_keys: updatedNoteKeys, + notes: updatedNotes, + }, + storeMetadata: { + ...state.storeMetadata, + annotationIsEdited: true, + }, + }; + }); + }, updateAnnotationNoteName: (note) => { set((state) => ({ ...state, diff --git a/compose/neurosynth-frontend/src/stores/AnnotationStore.types.ts b/compose/neurosynth-frontend/src/stores/AnnotationStore.types.ts index 59323c30b..6c9a06110 100644 --- a/compose/neurosynth-frontend/src/stores/AnnotationStore.types.ts +++ b/compose/neurosynth-frontend/src/stores/AnnotationStore.types.ts @@ -27,6 +27,7 @@ export type AnnotationStoreActions = { createAnnotationNote: (analysisId: string, studyId: string, analysisName: string) => void; deleteAnnotationNote: (analysisId: string) => void; updateAnnotationNoteName: (analysis: Partial) => void; + removeAnnotationColumn: (noteKey: string) => void; }; export type AnnotationNoteType = { From 980d2af48fa2f3a57033c49014af6ee88e99663e Mon Sep 17 00:00:00 2001 From: James Kent Date: Tue, 25 Nov 2025 08:50:55 -0600 Subject: [PATCH 3/5] add annotation table re-ordering logic and tests --- .../annotationsSingleSleuthStudyResponse.json | 12 +++- .../IngestionFixtures/annotationsFixture.json | 5 +- .../annotationsPutFixture.json | 5 +- .../cypress/fixtures/annotation.json | 12 +++- compose/neurosynth-frontend/package-lock.json | 43 ------------- .../components/HotTables/HotTables.types.ts | 1 + .../HotTables/HotTables.utils.spec.ts | 45 +++++++++++++ .../components/HotTables/HotTables.utils.tsx | 31 ++++++--- .../components/EditAnnotationsHotTable.tsx | 63 +++++++++++++++++-- .../components/SelectAnalysesComponent.tsx | 7 ++- .../components/MoveToExtractionDialog.tsx | 2 +- .../src/stores/AnnotationStore.helpers.ts | 17 +++-- .../src/stores/AnnotationStore.ts | 9 ++- .../src/testing/mockData.ts | 6 +- 14 files changed, 180 insertions(+), 78 deletions(-) create mode 100644 compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.spec.ts diff --git a/compose/neurosynth-frontend/cypress/fixtures/ImportSleuth/neurosynthResponses/annotationsSingleSleuthStudyResponse.json b/compose/neurosynth-frontend/cypress/fixtures/ImportSleuth/neurosynthResponses/annotationsSingleSleuthStudyResponse.json index f85ef62df..c50b57cff 100644 --- a/compose/neurosynth-frontend/cypress/fixtures/ImportSleuth/neurosynthResponses/annotationsSingleSleuthStudyResponse.json +++ b/compose/neurosynth-frontend/cypress/fixtures/ImportSleuth/neurosynthResponses/annotationsSingleSleuthStudyResponse.json @@ -25,10 +25,16 @@ "source_id": null, "source_updated_at": null, "note_keys": { - "included": "boolean", - "test1_new_txt": "boolean" + "included": { + "type": "boolean", + "order": 0 + }, + "test1_new_txt": { + "type": "boolean", + "order": 1 + } }, "metadata": null, "name": "Annotation for Untitled sleuth project", "description": "" -} \ No newline at end of file +} diff --git a/compose/neurosynth-frontend/cypress/fixtures/IngestionFixtures/annotationsFixture.json b/compose/neurosynth-frontend/cypress/fixtures/IngestionFixtures/annotationsFixture.json index fc1acb6a0..19056d2e1 100644 --- a/compose/neurosynth-frontend/cypress/fixtures/IngestionFixtures/annotationsFixture.json +++ b/compose/neurosynth-frontend/cypress/fixtures/IngestionFixtures/annotationsFixture.json @@ -10,7 +10,10 @@ "source_id": null, "source_updated_at": null, "note_keys": { - "included": "boolean" + "included": { + "type": "boolean", + "order": 0 + } }, "metadata": null, "name": "Annotation for studyset hQFjozWL9v8Q", diff --git a/compose/neurosynth-frontend/cypress/fixtures/IngestionFixtures/annotationsPutFixture.json b/compose/neurosynth-frontend/cypress/fixtures/IngestionFixtures/annotationsPutFixture.json index fc1acb6a0..19056d2e1 100644 --- a/compose/neurosynth-frontend/cypress/fixtures/IngestionFixtures/annotationsPutFixture.json +++ b/compose/neurosynth-frontend/cypress/fixtures/IngestionFixtures/annotationsPutFixture.json @@ -10,7 +10,10 @@ "source_id": null, "source_updated_at": null, "note_keys": { - "included": "boolean" + "included": { + "type": "boolean", + "order": 0 + } }, "metadata": null, "name": "Annotation for studyset hQFjozWL9v8Q", diff --git a/compose/neurosynth-frontend/cypress/fixtures/annotation.json b/compose/neurosynth-frontend/cypress/fixtures/annotation.json index 798bed3b2..c2753b970 100644 --- a/compose/neurosynth-frontend/cypress/fixtures/annotation.json +++ b/compose/neurosynth-frontend/cypress/fixtures/annotation.json @@ -5,8 +5,14 @@ "metadata": null, "name": "Annotation for studyset 73HRs8HaJbR8", "note_keys": { - "included": "boolean", - "string_key": "string" + "included": { + "type": "boolean", + "order": 0 + }, + "string_key": { + "type": "string", + "order": 1 + } }, "notes": [ { @@ -44,4 +50,4 @@ "user": "github|26612023", "username": "Nicholas Lee" } - \ No newline at end of file + diff --git a/compose/neurosynth-frontend/package-lock.json b/compose/neurosynth-frontend/package-lock.json index d827758c7..865b72507 100644 --- a/compose/neurosynth-frontend/package-lock.json +++ b/compose/neurosynth-frontend/package-lock.json @@ -9962,21 +9962,6 @@ } } }, - "node_modules/vite-tsconfig-paths/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/vitest": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", @@ -10283,19 +10268,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", @@ -17295,14 +17267,6 @@ "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", "dev": true, "requires": {} - }, - "typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "optional": true, - "peer": true } } }, @@ -17498,13 +17462,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "optional": true, - "peer": true - }, "yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", diff --git a/compose/neurosynth-frontend/src/components/HotTables/HotTables.types.ts b/compose/neurosynth-frontend/src/components/HotTables/HotTables.types.ts index a5e4840de..12236429d 100644 --- a/compose/neurosynth-frontend/src/components/HotTables/HotTables.types.ts +++ b/compose/neurosynth-frontend/src/components/HotTables/HotTables.types.ts @@ -3,6 +3,7 @@ import { EPropertyType } from 'components/EditMetadata/EditMetadata.types'; export interface NoteKeyType { key: string; type: EPropertyType; + order: number; } export type AnnotationNoteValue = string | number | boolean | null; diff --git a/compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.spec.ts b/compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.spec.ts new file mode 100644 index 000000000..bcd30bf04 --- /dev/null +++ b/compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.spec.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; +import { EPropertyType } from 'components/EditMetadata/EditMetadata.types'; +import { noteKeyArrToObj, noteKeyObjToArr } from './HotTables.utils'; + +describe('HotTables utils - note key conversions', () => { + it('converts note_keys object descriptors to a sorted array and reindexes order', () => { + const input = { + beta: { type: EPropertyType.STRING, order: 5 }, + alpha: { type: EPropertyType.NUMBER, order: 1 }, + gamma: { type: EPropertyType.BOOLEAN, order: 1 }, + }; + + const result = noteKeyObjToArr(input); + + expect(result).toEqual([ + { key: 'alpha', type: EPropertyType.NUMBER, order: 0 }, + { key: 'gamma', type: EPropertyType.BOOLEAN, order: 1 }, + { key: 'beta', type: EPropertyType.STRING, order: 2 }, + ]); + }); + + it('throws if a note_key descriptor is missing a type', () => { + const invalid = { + alpha: { order: 0 }, + } as any; + + expect(() => noteKeyObjToArr(invalid)).toThrow(/missing type/i); + }); + + it('converts a note key array back to descriptor object preserving order', () => { + const arr = [ + { key: 'first', type: EPropertyType.STRING, order: 2 }, + { key: 'second', type: EPropertyType.BOOLEAN, order: 0 }, + { key: 'third', type: EPropertyType.NUMBER, order: undefined as unknown as number }, + ]; + + const result = noteKeyArrToObj(arr); + + expect(result).toEqual({ + first: { type: EPropertyType.STRING, order: 2 }, + second: { type: EPropertyType.BOOLEAN, order: 0 }, + third: { type: EPropertyType.NUMBER, order: 2 }, + }); + }); +}); diff --git a/compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.tsx b/compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.tsx index 2ffd00cff..1883a021d 100644 --- a/compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.tsx +++ b/compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.tsx @@ -4,19 +4,32 @@ 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, - })); + const noteKeyTypes = noteKeys as { [key: string]: { type: EPropertyType; order?: number } }; + const arr = Object.entries(noteKeyTypes) + .map(([key, descriptor]) => { + if (!descriptor?.type) throw new Error('Invalid note_keys descriptor: missing type'); + return { + // rely on new descriptor shape (type + order) + type: descriptor.type, + key, + order: descriptor.order ?? 0, + }; + }) + .sort((a, b) => a.order - b.order || a.key.localeCompare(b.key)) + .map((noteKey, index) => ({ ...noteKey, order: index })); return arr; }; -export const noteKeyArrToObj = (noteKeyArr: NoteKeyType[]): { [key: string]: EPropertyType } => { - const noteKeyObj: { [key: string]: EPropertyType } = noteKeyArr.reduce((acc, curr) => { - acc[curr.key] = curr.type; +export const noteKeyArrToObj = ( + noteKeyArr: NoteKeyType[] +): { [key: string]: { type: EPropertyType; order: number } } => { + const noteKeyObj = noteKeyArr.reduce((acc, curr, index) => { + acc[curr.key] = { + type: curr.type, + order: curr.order ?? index, + }; return acc; - }, {} as { [key: string]: EPropertyType }); + }, {} as { [key: string]: { type: EPropertyType; order: number } }); return noteKeyObj; }; diff --git a/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.tsx index edd26110a..c982f7d7f 100644 --- a/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.tsx +++ b/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.tsx @@ -106,18 +106,23 @@ const AnnotationsHotTable: React.FC<{ annotationId?: string }> = React.memo((pro }; const handleRemoveHotColumn = (colKey: string) => { + if (!canEdit) return; 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); + const reindexedNoteKeys = updatedNoteKeys.map((noteKey, index) => ({ + ...noteKey, + order: index, + })); return { ...prev, isEdited: true, - noteKeys: updatedNoteKeys, - hotColumns: createColumns(updatedNoteKeys), + noteKeys: reindexedNoteKeys, + hotColumns: createColumns(reindexedNoteKeys, !canEdit), hotData: [...prev.hotData].map((row) => { const updatedRow = [...row]; updatedRow.splice(foundIndex + 2, 1); @@ -134,6 +139,52 @@ const AnnotationsHotTable: React.FC<{ annotationId?: string }> = React.memo((pro } }; + const reorderArray = (arr: T[], from: number, to: number) => { + if (from === to) return [...arr]; + const updated = [...arr]; + const [removed] = updated.splice(from, 1); + updated.splice(to, 0, removed); + return updated; + }; + + const handleColumnMove = (movedColumns: number[], finalIndex: number) => { + if (!canEdit) return; + if (!movedColumns.length) return; + const fromVisualIndex = movedColumns[0]; + const toVisualIndex = finalIndex; + + if (fromVisualIndex < 2 || toVisualIndex < 2) return; // lock study/analysis columns + + const from = fromVisualIndex - 2; + let to = toVisualIndex - 2; + + setAnnotationsHotState((prev) => { + if (from >= prev.noteKeys.length) return prev; + if (to >= prev.noteKeys.length) to = prev.noteKeys.length - 1; + + const updatedNoteKeys = reorderArray(prev.noteKeys, from, to).map((noteKey, index) => ({ + ...noteKey, + order: index, + })); + + const updatedHotData = prev.hotData.map((row) => { + const metadataCols = row.slice(0, 2); + const noteCols = reorderArray(row.slice(2), from, to); + return [...metadataCols, ...noteCols]; + }); + + return { + ...prev, + isEdited: true, + noteKeys: updatedNoteKeys, + hotColumns: createColumns(updatedNoteKeys, !canEdit), + hotData: updatedHotData, + }; + }); + + hotTableRef.current?.hotInstance?.getPlugin('manualColumnMove').clearMoves(); + }; + /** * 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 @@ -164,13 +215,15 @@ const AnnotationsHotTable: React.FC<{ annotationId?: string }> = React.memo((pro if (noteKeys.find((x) => x.key === trimmedKey)) return false; setAnnotationsHotState((prev) => { - const updatedNoteKeys = [{ key: trimmedKey, type: getType(row.metadataValue) }, ...prev.noteKeys]; + const updatedNoteKeys = [{ key: trimmedKey, type: getType(row.metadataValue), order: 0 }, ...prev.noteKeys].map( + (noteKey, index) => ({ ...noteKey, order: index }) + ); return { ...prev, isEdited: true, noteKeys: updatedNoteKeys, - hotColumns: createColumns(updatedNoteKeys), + hotColumns: createColumns(updatedNoteKeys, !canEdit), hotData: [...prev.hotData].map((row) => { const updatedRow = [...row]; updatedRow.splice(2, 0, null); @@ -239,12 +292,14 @@ const AnnotationsHotTable: React.FC<{ annotationId?: string }> = React.memo((pro mergeCells={mergeCells} disableVisualSelection={!canEdit} colHeaders={hotColumnHeaders} + manualColumnMove={canEdit} colWidths={colWidths} rowHeights={rowHeights} columns={hotColumns} data={JSON.parse(JSON.stringify(hotData))} afterOnCellMouseUp={handleCellMouseUp} beforeOnCellMouseDown={handleCellMouseDown} + afterColumnMove={handleColumnMove} /> ) : ( diff --git a/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/SelectAnalysesComponent.tsx b/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/SelectAnalysesComponent.tsx index 11e438575..57aeeae41 100644 --- a/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/SelectAnalysesComponent.tsx +++ b/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/SelectAnalysesComponent.tsx @@ -17,6 +17,7 @@ import { import { DEFAULT_REFERENCE_DATASETS } from './SelectAnalysesComponent.types'; import SelectAnalysesComponentTable from './SelectAnalysesComponentTable'; import SelectAnalysesStringValue from './SelectAnalysesStringValue'; +import { noteKeyObjToArr } from 'components/HotTables/HotTables.utils'; const SelectAnalysesComponent: React.FC<{ annotationId: string; @@ -58,10 +59,10 @@ const SelectAnalysesComponent: React.FC<{ ]); const options = useMemo(() => { - return Object.entries(annotation?.note_keys || {}) - .map(([key, value]) => ({ + return noteKeyObjToArr(annotation?.note_keys) + .map(({ key, type }) => ({ selectionKey: key, - type: value as EPropertyType, + type, selectionValue: undefined, referenceDataset: undefined, })) diff --git a/compose/neurosynth-frontend/src/pages/Project/components/MoveToExtractionDialog.tsx b/compose/neurosynth-frontend/src/pages/Project/components/MoveToExtractionDialog.tsx index 4012cfe4d..d61f055fe 100644 --- a/compose/neurosynth-frontend/src/pages/Project/components/MoveToExtractionDialog.tsx +++ b/compose/neurosynth-frontend/src/pages/Project/components/MoveToExtractionDialog.tsx @@ -105,7 +105,7 @@ const MoveToExtractionDialog: React.FC = (props) => { annotation: { name: `Annotation for studyset ${newStudysetId}`, description: '', - note_keys: { included: EPropertyType.BOOLEAN }, + note_keys: { included: { type: EPropertyType.BOOLEAN, order: 0 } }, studyset: newStudysetId, }, }); diff --git a/compose/neurosynth-frontend/src/stores/AnnotationStore.helpers.ts b/compose/neurosynth-frontend/src/stores/AnnotationStore.helpers.ts index 962cbb8a9..13890533d 100644 --- a/compose/neurosynth-frontend/src/stores/AnnotationStore.helpers.ts +++ b/compose/neurosynth-frontend/src/stores/AnnotationStore.helpers.ts @@ -5,11 +5,18 @@ import { NoteCollectionRequest } from 'neurostore-typescript-sdk'; 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, - })); + const noteKeyTypes = noteKeys as { [key: string]: { type: EPropertyType; order?: number } }; + const arr = Object.entries(noteKeyTypes) + .map(([key, descriptor]) => { + if (!descriptor?.type) throw new Error('Invalid note_keys descriptor: missing type'); + return { + key, + type: descriptor.type, + order: descriptor.order ?? 0, + }; + }) + .sort((a, b) => a.order - b.order || a.key.localeCompare(b.key)) + .map((noteKey, index) => ({ ...noteKey, order: index })); return arr; }; diff --git a/compose/neurosynth-frontend/src/stores/AnnotationStore.ts b/compose/neurosynth-frontend/src/stores/AnnotationStore.ts index d5ef7ebde..47854ab04 100644 --- a/compose/neurosynth-frontend/src/stores/AnnotationStore.ts +++ b/compose/neurosynth-frontend/src/stores/AnnotationStore.ts @@ -5,6 +5,7 @@ import { storeNotesToDBNotes, updateNoteNameHelper, } from 'stores/AnnotationStore.helpers'; +import { NoteKeyType } from 'components/HotTables/HotTables.types'; import API from 'utils/api'; import { create } from 'zustand'; import { @@ -16,6 +17,8 @@ import { import { setUnloadHandler } from 'helpers/BeforeUnload.helpers'; import { noteKeyArrToObj } from 'components/HotTables/HotTables.utils'; +const normalizeNoteKeyOrder = (noteKeys: NoteKeyType[]) => noteKeys.map((noteKey, index) => ({ ...noteKey, order: index })); + export const useAnnotationStore = create< { annotation: IStoreAnnotation; @@ -153,7 +156,7 @@ export const useAnnotationStore = create< ...state, annotation: { ...state.annotation, - note_keys: [{ ...noteKey }, ...(state.annotation.note_keys ?? [])], + note_keys: normalizeNoteKeyOrder([{ ...noteKey }, ...(state.annotation.note_keys ?? [])]), notes: (state.annotation.notes ?? []).map((note) => ({ ...note, note: { @@ -172,7 +175,9 @@ export const useAnnotationStore = create< setUnloadHandler('annotation'); set((state) => { if (!state.annotation.note_keys || !state.annotation.notes) return state; - const updatedNoteKeys = state.annotation.note_keys.filter((x) => x.key !== noteKey); + const updatedNoteKeys = normalizeNoteKeyOrder( + state.annotation.note_keys.filter((x) => x.key !== noteKey) + ); const updatedNotes = [...state.annotation.notes]; updatedNotes.forEach((note) => { const typedNote = note.note as Record | undefined; diff --git a/compose/neurosynth-frontend/src/testing/mockData.ts b/compose/neurosynth-frontend/src/testing/mockData.ts index 3d59bc564..d35bd5b11 100644 --- a/compose/neurosynth-frontend/src/testing/mockData.ts +++ b/compose/neurosynth-frontend/src/testing/mockData.ts @@ -221,9 +221,9 @@ const mockAnnotations: () => NeurostoreAnnotation[] = () => [ source: null, source_id: null, note_keys: { - inclusion_col: 'boolean', - aergegr: 'number', - aberg: 'string', + inclusion_col: { type: 'boolean', order: 0 }, + aergegr: { type: 'number', order: 1 }, + aberg: { type: 'string', order: 2 }, }, metadata: null, id: '62RUsQpwdouU', From 83ef98fe33784b29328f8bed3b24a6902e889150 Mon Sep 17 00:00:00 2001 From: James Kent Date: Tue, 25 Nov 2025 09:02:33 -0600 Subject: [PATCH 4/5] syncronize package.json --- compose/neurosynth-frontend/package-lock.json | 17 +++++++++-------- compose/neurosynth-frontend/package.json | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/compose/neurosynth-frontend/package-lock.json b/compose/neurosynth-frontend/package-lock.json index 865b72507..78e7188c4 100644 --- a/compose/neurosynth-frontend/package-lock.json +++ b/compose/neurosynth-frontend/package-lock.json @@ -76,7 +76,7 @@ "prettier": "^3.3.3", "react-error-overlay": "^6.0.9", "ts-node": "^10.9.2", - "typescript": "^4.9.5", + "typescript": "^5.9.3", "typescript-eslint": "^8.13.0", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.7" @@ -9656,16 +9656,17 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/typescript-eslint": { @@ -17109,9 +17110,9 @@ "dev": true }, "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true }, "typescript-eslint": { diff --git a/compose/neurosynth-frontend/package.json b/compose/neurosynth-frontend/package.json index 996fe4fb0..1a9cceec9 100644 --- a/compose/neurosynth-frontend/package.json +++ b/compose/neurosynth-frontend/package.json @@ -102,7 +102,7 @@ "prettier": "^3.3.3", "react-error-overlay": "^6.0.9", "ts-node": "^10.9.2", - "typescript": "^4.9.5", + "typescript": "^5.9.3", "typescript-eslint": "^8.13.0", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.7" From d4d6dbcc2ad696f40de72fbf029ed3ed8a72b5d1 Mon Sep 17 00:00:00 2001 From: Nicholas Lee Date: Tue, 25 Nov 2025 12:39:04 -0500 Subject: [PATCH 5/5] feat: minor fixes --- .../components/HotTables/HotTables.utils.tsx | 27 ++++----- .../components/EditAnnotationsHotTable.tsx | 55 ++----------------- .../components/SelectAnalysesComponent.tsx | 29 ++-------- .../components/MoveToExtractionDialog.tsx | 2 - .../src/stores/AnnotationStore.ts | 3 +- 5 files changed, 24 insertions(+), 92 deletions(-) diff --git a/compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.tsx b/compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.tsx index 1883a021d..2f35f9907 100644 --- a/compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.tsx +++ b/compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.tsx @@ -9,7 +9,6 @@ export const noteKeyObjToArr = (noteKeys?: object | null): NoteKeyType[] => { .map(([key, descriptor]) => { if (!descriptor?.type) throw new Error('Invalid note_keys descriptor: missing type'); return { - // rely on new descriptor shape (type + order) type: descriptor.type, key, order: descriptor.order ?? 0, @@ -23,25 +22,23 @@ export const noteKeyObjToArr = (noteKeys?: object | null): NoteKeyType[] => { export const noteKeyArrToObj = ( noteKeyArr: NoteKeyType[] ): { [key: string]: { type: EPropertyType; order: number } } => { - const noteKeyObj = noteKeyArr.reduce((acc, curr, index) => { - acc[curr.key] = { - type: curr.type, - order: curr.order ?? index, - }; - return acc; - }, {} as { [key: string]: { type: EPropertyType; order: number } }); + const noteKeyObj = noteKeyArr.reduce( + (acc, curr, index) => { + acc[curr.key] = { + type: curr.type, + order: curr.order ?? index, + }; + return acc; + }, + {} as { [key: string]: { type: EPropertyType; order: number } } + ); return noteKeyObj; }; export const booleanValidator = (value: CellValue, callback: (isValid: boolean) => void) => { const isValid = - value === true || - value === false || - value === 'true' || - value === 'false' || - value === null || - value === ''; + value === true || value === false || value === 'true' || value === 'false' || value === null || value === ''; callback(isValid); }; @@ -54,7 +51,7 @@ export const replaceString = (val: string) => { export const stripTags = (stringWhichMayHaveHTML: any) => { if (typeof stringWhichMayHaveHTML !== 'string') return ''; - let doc = new DOMParser().parseFromString(stringWhichMayHaveHTML, 'text/html'); + const doc = new DOMParser().parseFromString(stringWhichMayHaveHTML, 'text/html'); return doc.body.textContent || ''; }; diff --git a/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.tsx index c982f7d7f..129e5147d 100644 --- a/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.tsx +++ b/compose/neurosynth-frontend/src/pages/Annotations/components/EditAnnotationsHotTable.tsx @@ -139,52 +139,6 @@ const AnnotationsHotTable: React.FC<{ annotationId?: string }> = React.memo((pro } }; - const reorderArray = (arr: T[], from: number, to: number) => { - if (from === to) return [...arr]; - const updated = [...arr]; - const [removed] = updated.splice(from, 1); - updated.splice(to, 0, removed); - return updated; - }; - - const handleColumnMove = (movedColumns: number[], finalIndex: number) => { - if (!canEdit) return; - if (!movedColumns.length) return; - const fromVisualIndex = movedColumns[0]; - const toVisualIndex = finalIndex; - - if (fromVisualIndex < 2 || toVisualIndex < 2) return; // lock study/analysis columns - - const from = fromVisualIndex - 2; - let to = toVisualIndex - 2; - - setAnnotationsHotState((prev) => { - if (from >= prev.noteKeys.length) return prev; - if (to >= prev.noteKeys.length) to = prev.noteKeys.length - 1; - - const updatedNoteKeys = reorderArray(prev.noteKeys, from, to).map((noteKey, index) => ({ - ...noteKey, - order: index, - })); - - const updatedHotData = prev.hotData.map((row) => { - const metadataCols = row.slice(0, 2); - const noteCols = reorderArray(row.slice(2), from, to); - return [...metadataCols, ...noteCols]; - }); - - return { - ...prev, - isEdited: true, - noteKeys: updatedNoteKeys, - hotColumns: createColumns(updatedNoteKeys, !canEdit), - hotData: updatedHotData, - }; - }); - - hotTableRef.current?.hotInstance?.getPlugin('manualColumnMove').clearMoves(); - }; - /** * 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 @@ -215,9 +169,10 @@ const AnnotationsHotTable: React.FC<{ annotationId?: string }> = React.memo((pro if (noteKeys.find((x) => x.key === trimmedKey)) return false; setAnnotationsHotState((prev) => { - const updatedNoteKeys = [{ key: trimmedKey, type: getType(row.metadataValue), order: 0 }, ...prev.noteKeys].map( - (noteKey, index) => ({ ...noteKey, order: index }) - ); + const updatedNoteKeys = [ + { key: trimmedKey, type: getType(row.metadataValue), order: 0 }, + ...prev.noteKeys, + ].map((noteKey, index) => ({ ...noteKey, order: index })); return { ...prev, @@ -292,14 +247,12 @@ const AnnotationsHotTable: React.FC<{ annotationId?: string }> = React.memo((pro mergeCells={mergeCells} disableVisualSelection={!canEdit} colHeaders={hotColumnHeaders} - manualColumnMove={canEdit} colWidths={colWidths} rowHeights={rowHeights} columns={hotColumns} data={JSON.parse(JSON.stringify(hotData))} afterOnCellMouseUp={handleCellMouseUp} beforeOnCellMouseDown={handleCellMouseDown} - afterColumnMove={handleColumnMove} /> ) : ( diff --git a/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/SelectAnalysesComponent.tsx b/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/SelectAnalysesComponent.tsx index 57aeeae41..913153186 100644 --- a/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/SelectAnalysesComponent.tsx +++ b/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/SelectAnalysesComponent.tsx @@ -10,10 +10,7 @@ import { IAlgorithmSelection, IAnalysesSelection, } from 'pages/MetaAnalysis/components/CreateMetaAnalysisSpecificationDialogBase.types'; -import { - isMultiGroupAlgorithm, - selectedReferenceDatasetIsDefaultDataset, -} from './SelectAnalysesComponent.helpers'; +import { isMultiGroupAlgorithm, selectedReferenceDatasetIsDefaultDataset } from './SelectAnalysesComponent.helpers'; import { DEFAULT_REFERENCE_DATASETS } from './SelectAnalysesComponent.types'; import SelectAnalysesComponentTable from './SelectAnalysesComponentTable'; import SelectAnalysesStringValue from './SelectAnalysesStringValue'; @@ -50,13 +47,7 @@ const SelectAnalysesComponent: React.FC<{ onSelectValue(initialVal); selectionOccurred.current = true; } - }, [ - selectedValue.selectionKey, - annotation, - onSelectValue, - selectionOccurred, - algorithm?.estimator, - ]); + }, [selectedValue.selectionKey, annotation, onSelectValue, selectionOccurred, algorithm?.estimator]); const options = useMemo(() => { return noteKeyObjToArr(annotation?.note_keys) @@ -77,17 +68,13 @@ const SelectAnalysesComponent: React.FC<{ const handleSelectColumn = (newVal: IAnalysesSelection | undefined) => { if (newVal?.selectionKey === selectedValue.selectionKey) return; // we selected the same option that is already selected - const referenceDatasetIsNowInvalid = !selectedReferenceDatasetIsDefaultDataset( - selectedValue.referenceDataset - ); + const referenceDatasetIsNowInvalid = !selectedReferenceDatasetIsDefaultDataset(selectedValue.referenceDataset); if (!newVal) { onSelectValue({ selectionKey: undefined, type: undefined, selectionValue: undefined, - referenceDataset: referenceDatasetIsNowInvalid - ? undefined - : selectedValue.referenceDataset, + referenceDataset: referenceDatasetIsNowInvalid ? undefined : selectedValue.referenceDataset, }); return; } @@ -96,9 +83,7 @@ const SelectAnalysesComponent: React.FC<{ selectionKey: newVal.selectionKey, type: newVal.type, selectionValue: newVal.type === EPropertyType.BOOLEAN ? true : undefined, - referenceDataset: referenceDatasetIsNowInvalid - ? undefined - : selectedValue.referenceDataset, + referenceDataset: referenceDatasetIsNowInvalid ? undefined : selectedValue.referenceDataset, }; onSelectValue(update); }; @@ -109,9 +94,7 @@ const SelectAnalysesComponent: React.FC<{ sx={CreateMetaAnalysisSpecificationDialogBaseStyles.highlightInput} label="Inclusion Column" shouldDisable={false} - isOptionEqualToValue={(option, value) => - option?.selectionKey === value?.selectionKey - } + isOptionEqualToValue={(option, value) => option?.selectionKey === value?.selectionKey} value={selectedValue?.selectionKey ? selectedValue : undefined} size="medium" inputPropsSx={{ diff --git a/compose/neurosynth-frontend/src/pages/Project/components/MoveToExtractionDialog.tsx b/compose/neurosynth-frontend/src/pages/Project/components/MoveToExtractionDialog.tsx index d61f055fe..8e7c2b5f5 100644 --- a/compose/neurosynth-frontend/src/pages/Project/components/MoveToExtractionDialog.tsx +++ b/compose/neurosynth-frontend/src/pages/Project/components/MoveToExtractionDialog.tsx @@ -42,7 +42,6 @@ const MoveToExtractionDialog: React.FC = (props) => { const navigate = useNavigate(); - const [isLoadingPhase, setIsLoadingPhase] = useState(false); const [isError, setIsError] = useState(false); const [loadingStatus, setLoadingStatus] = useState<{ createdStudyset: boolean; @@ -55,7 +54,6 @@ const MoveToExtractionDialog: React.FC = (props) => { }); const handleCloseDialog = () => { - setIsLoadingPhase(false); setLoadingStatus({ createdAnnotations: false, createdStudyset: false, diff --git a/compose/neurosynth-frontend/src/stores/AnnotationStore.ts b/compose/neurosynth-frontend/src/stores/AnnotationStore.ts index 47854ab04..7166c9eb3 100644 --- a/compose/neurosynth-frontend/src/stores/AnnotationStore.ts +++ b/compose/neurosynth-frontend/src/stores/AnnotationStore.ts @@ -17,7 +17,8 @@ import { import { setUnloadHandler } from 'helpers/BeforeUnload.helpers'; import { noteKeyArrToObj } from 'components/HotTables/HotTables.utils'; -const normalizeNoteKeyOrder = (noteKeys: NoteKeyType[]) => noteKeys.map((noteKey, index) => ({ ...noteKey, order: index })); +const normalizeNoteKeyOrder = (noteKeys: NoteKeyType[]) => + noteKeys.map((noteKey, index) => ({ ...noteKey, order: index })); export const useAnnotationStore = create< {