Skip to content

Commit 980d2af

Browse files
committed
add annotation table re-ordering logic and tests
1 parent 56a4169 commit 980d2af

File tree

14 files changed

+180
-78
lines changed

14 files changed

+180
-78
lines changed

compose/neurosynth-frontend/cypress/fixtures/ImportSleuth/neurosynthResponses/annotationsSingleSleuthStudyResponse.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,16 @@
2525
"source_id": null,
2626
"source_updated_at": null,
2727
"note_keys": {
28-
"included": "boolean",
29-
"test1_new_txt": "boolean"
28+
"included": {
29+
"type": "boolean",
30+
"order": 0
31+
},
32+
"test1_new_txt": {
33+
"type": "boolean",
34+
"order": 1
35+
}
3036
},
3137
"metadata": null,
3238
"name": "Annotation for Untitled sleuth project",
3339
"description": ""
34-
}
40+
}

compose/neurosynth-frontend/cypress/fixtures/IngestionFixtures/annotationsFixture.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
"source_id": null,
1111
"source_updated_at": null,
1212
"note_keys": {
13-
"included": "boolean"
13+
"included": {
14+
"type": "boolean",
15+
"order": 0
16+
}
1417
},
1518
"metadata": null,
1619
"name": "Annotation for studyset hQFjozWL9v8Q",

compose/neurosynth-frontend/cypress/fixtures/IngestionFixtures/annotationsPutFixture.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
"source_id": null,
1111
"source_updated_at": null,
1212
"note_keys": {
13-
"included": "boolean"
13+
"included": {
14+
"type": "boolean",
15+
"order": 0
16+
}
1417
},
1518
"metadata": null,
1619
"name": "Annotation for studyset hQFjozWL9v8Q",

compose/neurosynth-frontend/cypress/fixtures/annotation.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@
55
"metadata": null,
66
"name": "Annotation for studyset 73HRs8HaJbR8",
77
"note_keys": {
8-
"included": "boolean",
9-
"string_key": "string"
8+
"included": {
9+
"type": "boolean",
10+
"order": 0
11+
},
12+
"string_key": {
13+
"type": "string",
14+
"order": 1
15+
}
1016
},
1117
"notes": [
1218
{
@@ -44,4 +50,4 @@
4450
"user": "github|26612023",
4551
"username": "Nicholas Lee"
4652
}
47-
53+

compose/neurosynth-frontend/package-lock.json

Lines changed: 0 additions & 43 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

compose/neurosynth-frontend/src/components/HotTables/HotTables.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { EPropertyType } from 'components/EditMetadata/EditMetadata.types';
33
export interface NoteKeyType {
44
key: string;
55
type: EPropertyType;
6+
order: number;
67
}
78

89
export type AnnotationNoteValue = string | number | boolean | null;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { EPropertyType } from 'components/EditMetadata/EditMetadata.types';
3+
import { noteKeyArrToObj, noteKeyObjToArr } from './HotTables.utils';
4+
5+
describe('HotTables utils - note key conversions', () => {
6+
it('converts note_keys object descriptors to a sorted array and reindexes order', () => {
7+
const input = {
8+
beta: { type: EPropertyType.STRING, order: 5 },
9+
alpha: { type: EPropertyType.NUMBER, order: 1 },
10+
gamma: { type: EPropertyType.BOOLEAN, order: 1 },
11+
};
12+
13+
const result = noteKeyObjToArr(input);
14+
15+
expect(result).toEqual([
16+
{ key: 'alpha', type: EPropertyType.NUMBER, order: 0 },
17+
{ key: 'gamma', type: EPropertyType.BOOLEAN, order: 1 },
18+
{ key: 'beta', type: EPropertyType.STRING, order: 2 },
19+
]);
20+
});
21+
22+
it('throws if a note_key descriptor is missing a type', () => {
23+
const invalid = {
24+
alpha: { order: 0 },
25+
} as any;
26+
27+
expect(() => noteKeyObjToArr(invalid)).toThrow(/missing type/i);
28+
});
29+
30+
it('converts a note key array back to descriptor object preserving order', () => {
31+
const arr = [
32+
{ key: 'first', type: EPropertyType.STRING, order: 2 },
33+
{ key: 'second', type: EPropertyType.BOOLEAN, order: 0 },
34+
{ key: 'third', type: EPropertyType.NUMBER, order: undefined as unknown as number },
35+
];
36+
37+
const result = noteKeyArrToObj(arr);
38+
39+
expect(result).toEqual({
40+
first: { type: EPropertyType.STRING, order: 2 },
41+
second: { type: EPropertyType.BOOLEAN, order: 0 },
42+
third: { type: EPropertyType.NUMBER, order: 2 },
43+
});
44+
});
45+
});

compose/neurosynth-frontend/src/components/HotTables/HotTables.utils.tsx

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,32 @@ import { CellValue } from 'handsontable/common';
44

55
export const noteKeyObjToArr = (noteKeys?: object | null): NoteKeyType[] => {
66
if (!noteKeys) return [];
7-
const noteKeyTypes = noteKeys as { [key: string]: EPropertyType };
8-
const arr = Object.entries(noteKeyTypes).map(([key, type]) => ({
9-
key,
10-
type,
11-
}));
7+
const noteKeyTypes = noteKeys as { [key: string]: { type: EPropertyType; order?: number } };
8+
const arr = Object.entries(noteKeyTypes)
9+
.map(([key, descriptor]) => {
10+
if (!descriptor?.type) throw new Error('Invalid note_keys descriptor: missing type');
11+
return {
12+
// rely on new descriptor shape (type + order)
13+
type: descriptor.type,
14+
key,
15+
order: descriptor.order ?? 0,
16+
};
17+
})
18+
.sort((a, b) => a.order - b.order || a.key.localeCompare(b.key))
19+
.map((noteKey, index) => ({ ...noteKey, order: index }));
1220
return arr;
1321
};
1422

15-
export const noteKeyArrToObj = (noteKeyArr: NoteKeyType[]): { [key: string]: EPropertyType } => {
16-
const noteKeyObj: { [key: string]: EPropertyType } = noteKeyArr.reduce((acc, curr) => {
17-
acc[curr.key] = curr.type;
23+
export const noteKeyArrToObj = (
24+
noteKeyArr: NoteKeyType[]
25+
): { [key: string]: { type: EPropertyType; order: number } } => {
26+
const noteKeyObj = noteKeyArr.reduce((acc, curr, index) => {
27+
acc[curr.key] = {
28+
type: curr.type,
29+
order: curr.order ?? index,
30+
};
1831
return acc;
19-
}, {} as { [key: string]: EPropertyType });
32+
}, {} as { [key: string]: { type: EPropertyType; order: number } });
2033

2134
return noteKeyObj;
2235
};

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

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,18 +106,23 @@ const AnnotationsHotTable: React.FC<{ annotationId?: string }> = React.memo((pro
106106
};
107107

108108
const handleRemoveHotColumn = (colKey: string) => {
109+
if (!canEdit) return;
109110
const foundIndex = noteKeys.findIndex((x) => x.key === colKey && x.key !== 'included');
110111
if (foundIndex < 0) return;
111112

112113
setAnnotationsHotState((prev) => {
113114
const updatedNoteKeys = [...prev.noteKeys];
114115
updatedNoteKeys.splice(foundIndex, 1);
116+
const reindexedNoteKeys = updatedNoteKeys.map((noteKey, index) => ({
117+
...noteKey,
118+
order: index,
119+
}));
115120

116121
return {
117122
...prev,
118123
isEdited: true,
119-
noteKeys: updatedNoteKeys,
120-
hotColumns: createColumns(updatedNoteKeys),
124+
noteKeys: reindexedNoteKeys,
125+
hotColumns: createColumns(reindexedNoteKeys, !canEdit),
121126
hotData: [...prev.hotData].map((row) => {
122127
const updatedRow = [...row];
123128
updatedRow.splice(foundIndex + 2, 1);
@@ -134,6 +139,52 @@ const AnnotationsHotTable: React.FC<{ annotationId?: string }> = React.memo((pro
134139
}
135140
};
136141

142+
const reorderArray = <T,>(arr: T[], from: number, to: number) => {
143+
if (from === to) return [...arr];
144+
const updated = [...arr];
145+
const [removed] = updated.splice(from, 1);
146+
updated.splice(to, 0, removed);
147+
return updated;
148+
};
149+
150+
const handleColumnMove = (movedColumns: number[], finalIndex: number) => {
151+
if (!canEdit) return;
152+
if (!movedColumns.length) return;
153+
const fromVisualIndex = movedColumns[0];
154+
const toVisualIndex = finalIndex;
155+
156+
if (fromVisualIndex < 2 || toVisualIndex < 2) return; // lock study/analysis columns
157+
158+
const from = fromVisualIndex - 2;
159+
let to = toVisualIndex - 2;
160+
161+
setAnnotationsHotState((prev) => {
162+
if (from >= prev.noteKeys.length) return prev;
163+
if (to >= prev.noteKeys.length) to = prev.noteKeys.length - 1;
164+
165+
const updatedNoteKeys = reorderArray(prev.noteKeys, from, to).map((noteKey, index) => ({
166+
...noteKey,
167+
order: index,
168+
}));
169+
170+
const updatedHotData = prev.hotData.map((row) => {
171+
const metadataCols = row.slice(0, 2);
172+
const noteCols = reorderArray(row.slice(2), from, to);
173+
return [...metadataCols, ...noteCols];
174+
});
175+
176+
return {
177+
...prev,
178+
isEdited: true,
179+
noteKeys: updatedNoteKeys,
180+
hotColumns: createColumns(updatedNoteKeys, !canEdit),
181+
hotData: updatedHotData,
182+
};
183+
});
184+
185+
hotTableRef.current?.hotInstance?.getPlugin('manualColumnMove').clearMoves();
186+
};
187+
137188
/**
138189
* 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
139190
* 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
164215
if (noteKeys.find((x) => x.key === trimmedKey)) return false;
165216

166217
setAnnotationsHotState((prev) => {
167-
const updatedNoteKeys = [{ key: trimmedKey, type: getType(row.metadataValue) }, ...prev.noteKeys];
218+
const updatedNoteKeys = [{ key: trimmedKey, type: getType(row.metadataValue), order: 0 }, ...prev.noteKeys].map(
219+
(noteKey, index) => ({ ...noteKey, order: index })
220+
);
168221

169222
return {
170223
...prev,
171224
isEdited: true,
172225
noteKeys: updatedNoteKeys,
173-
hotColumns: createColumns(updatedNoteKeys),
226+
hotColumns: createColumns(updatedNoteKeys, !canEdit),
174227
hotData: [...prev.hotData].map((row) => {
175228
const updatedRow = [...row];
176229
updatedRow.splice(2, 0, null);
@@ -239,12 +292,14 @@ const AnnotationsHotTable: React.FC<{ annotationId?: string }> = React.memo((pro
239292
mergeCells={mergeCells}
240293
disableVisualSelection={!canEdit}
241294
colHeaders={hotColumnHeaders}
295+
manualColumnMove={canEdit}
242296
colWidths={colWidths}
243297
rowHeights={rowHeights}
244298
columns={hotColumns}
245299
data={JSON.parse(JSON.stringify(hotData))}
246300
afterOnCellMouseUp={handleCellMouseUp}
247301
beforeOnCellMouseDown={handleCellMouseDown}
302+
afterColumnMove={handleColumnMove}
248303
/>
249304
) : (
250305
<Typography sx={{ color: 'warning.dark' }}>

compose/neurosynth-frontend/src/pages/MetaAnalysis/components/SelectAnalysesComponent.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { DEFAULT_REFERENCE_DATASETS } from './SelectAnalysesComponent.types';
1818
import SelectAnalysesComponentTable from './SelectAnalysesComponentTable';
1919
import SelectAnalysesStringValue from './SelectAnalysesStringValue';
20+
import { noteKeyObjToArr } from 'components/HotTables/HotTables.utils';
2021

2122
const SelectAnalysesComponent: React.FC<{
2223
annotationId: string;
@@ -58,10 +59,10 @@ const SelectAnalysesComponent: React.FC<{
5859
]);
5960

6061
const options = useMemo(() => {
61-
return Object.entries(annotation?.note_keys || {})
62-
.map(([key, value]) => ({
62+
return noteKeyObjToArr(annotation?.note_keys)
63+
.map(({ key, type }) => ({
6364
selectionKey: key,
64-
type: value as EPropertyType,
65+
type,
6566
selectionValue: undefined,
6667
referenceDataset: undefined,
6768
}))

0 commit comments

Comments
 (0)