Skip to content

Commit 42e1b9d

Browse files
authored
Merge pull request #176 from MetaCell/feature/pre_cell_card_review
GH issues
2 parents d8e793a + bf2b314 commit 42e1b9d

13 files changed

Lines changed: 242 additions & 63 deletions

File tree

src/App.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
BrowserRouter as Router,
66
Routes,
77
Route,
8+
Navigate,
89
useLocation,
910
useNavigate
1011
} from "react-router-dom";
@@ -223,6 +224,7 @@ function MainContent() {
223224
</PageContainer>
224225
}
225226
/>
227+
<Route path="*" element={<Navigate to="/" replace />} />
226228
</Routes>
227229
</Layout>
228230
</Box>

src/api/endpoints/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import curieParser from '../../parsers/curieParser';
66
import termParser, { elasticSearchParser, getTerm } from '../../parsers/termParser';
77
import axios from 'axios';
88
import { API_CONFIG } from '../../config';
9+
import { reportApiError } from '../apiErrorBus';
910

1011
const useApi = () => api;
1112
const useMockApi = () => mockApi;
@@ -148,6 +149,13 @@ const fetchData = async (url, method = "GET", data: object | null = null) => {
148149
},
149150
withCredentials : true
150151
});
152+
// Some backends (e.g. the Elasticsearch proxy) return HTTP 200 with
153+
// the failure encoded in the body, so axios never rejects on its own.
154+
if (response.data?.error) {
155+
const bodyError: any = new Error(response.data.error.message || `Request failed with code ${response.data.error.code}`);
156+
bodyError.status = response.data.error.code;
157+
throw bodyError;
158+
}
151159
return response.data;
152160
} catch (error) {
153161
console.error(`API Error at ${url}:`, error);
@@ -175,6 +183,12 @@ export const elasticSearch = async (
175183
total = initialResponse?.hits?.total ?? 0;
176184
} catch (error) {
177185
console.error("Failed to fetch total count from Elasticsearch:", error);
186+
reportApiError({
187+
context: `Search "${query}"`,
188+
url,
189+
status: error?.status ?? error?.response?.status,
190+
message: error?.message || "Request failed",
191+
});
178192
return { results: [], total: 0 };
179193
}
180194
}
@@ -192,6 +206,12 @@ export const elasticSearch = async (
192206
};
193207
} catch (error) {
194208
console.error("Error when performing elastic search", error);
209+
reportApiError({
210+
context: `Search "${query}"`,
211+
url,
212+
status: error?.status ?? error?.response?.status,
213+
message: error?.message || "Request failed",
214+
});
195215
return { results: [], total: 0 };
196216
}
197217
};

src/components/Auth/Register.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { API_CONFIG } from "../../config";
1616
import { useCookies } from 'react-cookie';
1717
import PasswordField from "./UI/PasswordField";
1818
import { ArrowBack } from "@mui/icons-material";
19-
import { Link, useNavigate } from "react-router-dom";
19+
import { Link, useNavigate, useLocation } from "react-router-dom";
2020
// import { GlobalDataContext } from "../../contexts/DataContext";
2121

2222
const OLYMPIAN_GODS = import.meta.env.MODE === "production" ? "" : API_CONFIG.OLYMPIAN_GODS;
@@ -54,11 +54,12 @@ const Register = () => {
5454
const [existingCookies, setCookie, removeCookie] = useCookies(['session']);
5555
const prevSnackbarOpen = React.useRef(snackbarOpen);
5656
const navigate = useNavigate();
57+
const location = useLocation();
5758

5859
React.useEffect(() => {
5960
if (prevSnackbarOpen.current && !snackbarOpen) {
6061
closePopups();
61-
navigate("/login");
62+
navigate("/login", { state: { from: location.state?.from } });
6263
}
6364
prevSnackbarOpen.current = snackbarOpen;
6465
// eslint-disable-next-line react-hooks/exhaustive-deps

src/components/CurieEditor/CuriesTabPanel.jsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,39 @@ const CuriesTabPanel = (props) => {
5353
const [columnIndex, setColumnIndex] = React.useState(-1);
5454
const [order, setOrder] = React.useState('asc');
5555
const [orderBy, setOrderBy] = React.useState('prefix');
56+
const [displayOrder, setDisplayOrder] = React.useState([]);
57+
const isEditing = rowId !== null;
5658

57-
const sortedRows = React.useMemo(() => {
59+
const sortIds = (rowsArr) => stableSort(rowsArr, getComparator(order, orderBy)).map((row) => row._id);
60+
61+
// Manual header sort always re-orders, even mid-edit.
62+
React.useEffect(() => {
63+
setDisplayOrder(sortIds(Array.isArray(rows) ? rows : []));
64+
// eslint-disable-next-line react-hooks/exhaustive-deps
65+
}, [order, orderBy]);
66+
67+
// Row content changes (typing, add/delete) only trigger a full re-sort once
68+
// editing is done; while editing, just reconcile which rows exist so a row
69+
// doesn't jump position under the user as soon as it gets a value.
70+
React.useEffect(() => {
5871
const safeRows = Array.isArray(rows) ? rows : [];
59-
return stableSort(safeRows, getComparator(order, orderBy));
60-
}, [rows, order, orderBy]);
72+
if (!isEditing) {
73+
setDisplayOrder(sortIds(safeRows));
74+
return;
75+
}
76+
const currentIds = safeRows.map((row) => row._id);
77+
setDisplayOrder((prevOrder) => {
78+
const stillPresent = prevOrder.filter((id) => currentIds.includes(id));
79+
const newIds = currentIds.filter((id) => !prevOrder.includes(id));
80+
return [...stillPresent, ...newIds];
81+
});
82+
// eslint-disable-next-line react-hooks/exhaustive-deps
83+
}, [rows, isEditing]);
84+
85+
const sortedRows = React.useMemo(() => {
86+
const rowsById = new Map((Array.isArray(rows) ? rows : []).map((row) => [row._id, row]));
87+
return displayOrder.map((id) => rowsById.get(id)).filter(Boolean);
88+
}, [displayOrder, rows]);
6189

6290
React.useEffect(() => {
6391
onCurieAmountChange?.(rows.length)

src/components/CurieEditor/index.jsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ const CurieEditor = () => {
3030
setOpenCurieEditor(true);
3131
};
3232

33-
const handleCloseCurieEditor = () => setOpenCurieEditor(false);
33+
const handleCloseCurieEditor = () => {
34+
// Discard any unsaved edits/added rows made in the dialog.
35+
setLocalCuries(curies);
36+
setOpenCurieEditor(false);
37+
};
3438
const handleChangeTabs = (event, newValue) => setTabValue(newValue);
3539
const handleCurieAmountChange = (value) => setCurieAmount(value);
3640

@@ -126,7 +130,6 @@ const CurieEditor = () => {
126130
loading={curiesLoading}
127131
editMode={tab === 'base'}
128132
rows={localCuries[tab]}
129-
onCurieAmountChange={handleCurieAmountChange}
130133
onAddRow={handleAddNewCurieRow}
131134
onDeleteRow={handleDeleteCurieRow}
132135
onChangeRow={handleInputChangeCurieRow}

src/components/Header/Search.jsx

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Chip,
1111
List,
1212
ListItem,
13+
LinearProgress,
1314
} from "@mui/material";
1415
import { debounce } from 'lodash';
1516
import PropTypes from 'prop-types';
@@ -91,6 +92,7 @@ const Search = () => {
9192
const [terms, setTerms] = useState([]);
9293
const [organizations, setOrganizations] = useState([]);
9394
const [ontologies, setOntologies] = useState([]);
95+
const [isSearching, setIsSearching] = useState(false);
9496
const { storedSearchTerm, updateStoredSearchTerm, user } = useContext(GlobalDataContext);
9597

9698
// Get the group name based on user login status
@@ -155,6 +157,7 @@ const Search = () => {
155157
setTerms([])
156158
setOntologies([])
157159
setOrganizations([])
160+
setIsSearching(false);
158161
};
159162

160163
const escapeSearch = useCallback(() => {
@@ -164,11 +167,14 @@ const Search = () => {
164167
setTerms([])
165168
setOntologies([])
166169
setOrganizations([])
170+
setIsSearching(false);
167171
}, []);
168172

169173
const handleKeyDown = useCallback(event => {
170-
if (event.ctrlKey && event.key === 'k') {
174+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
175+
event.preventDefault();
171176
setOpenList(true);
177+
document.getElementById('interlex-search-input')?.focus();
172178
}
173179
if (event.key === 'Escape') {
174180
escapeSearch();
@@ -182,13 +188,18 @@ const Search = () => {
182188

183189
// eslint-disable-next-line react-hooks/exhaustive-deps
184190
const fetchTerms = useCallback(debounce(async (searchTerm) => {
185-
const data = await elasticSearch(searchTerm, 20, 0);
186-
const dataTerms = data?.results.results?.filter(result => result.type === SEARCH_TYPES.TERM);
187-
const dataOrganizations = data?.results.results?.filter(result => result.type === SEARCH_TYPES.ORGANIZATION);
188-
const dataOntologies = data?.results.results?.filter(result => result.type === SEARCH_TYPES.ONTOLOGY);
189-
setTerms(dataTerms);
190-
setOrganizations(dataOrganizations);
191-
setOntologies(dataOntologies);
191+
setIsSearching(true);
192+
try {
193+
const data = await elasticSearch(searchTerm, 20, 0);
194+
const dataTerms = data?.results.results?.filter(result => result.type === SEARCH_TYPES.TERM);
195+
const dataOrganizations = data?.results.results?.filter(result => result.type === SEARCH_TYPES.ORGANIZATION);
196+
const dataOntologies = data?.results.results?.filter(result => result.type === SEARCH_TYPES.ONTOLOGY);
197+
setTerms(dataTerms);
198+
setOrganizations(dataOrganizations);
199+
setOntologies(dataOntologies);
200+
} finally {
201+
setIsSearching(false);
202+
}
192203
}, 500), [searchAll]);
193204

194205
useEffect(() => {
@@ -208,6 +219,7 @@ const Search = () => {
208219
const ListboxComponent = forwardRef(function ListboxComponent(props, ref) {
209220
return (
210221
<>
222+
{isSearching && <LinearProgress sx={{ height: '0.125rem' }} />}
211223
{searchTerm && (<><Box p="0.5rem">
212224
<List sx={{
213225
'& .MuiTypography-body1': {
@@ -364,6 +376,10 @@ const Search = () => {
364376
placeholder="Find something..."
365377
onChange={handleInputChange}
366378
onKeyDown={handleEnterKey}
379+
inputProps={{
380+
...params.inputProps,
381+
id: 'interlex-search-input',
382+
}}
367383
InputProps={{
368384
...params.InputProps,
369385
startAdornment: (
@@ -373,15 +389,15 @@ const Search = () => {
373389
),
374390
endAdornment: (
375391
<InputAdornment position="end">
376-
{openList ? (
392+
{searchTerm ? (
377393
<Box display="flex" alignItems="center" gap="0.75rem">
378394
<IconButton
379395
sx={styles.searchButton}
380396
onClick={resetSearch}
381397
>
382398
<CloseIcon />
383399
</IconButton>
384-
<Box sx={styles.keyBoardInfo}>Esc</Box>
400+
{openList && <Box sx={styles.keyBoardInfo}>Esc</Box>}
385401
</Box>
386402
) : (
387403
<Box sx={styles.keyBoardInfo}>Ctrl + K</Box>

src/components/Header/index.jsx

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import Search from './Search';
2323
import { useContext } from "react";
2424
import List from '@mui/material/List';
2525
import ListItem from '@mui/material/ListItem';
26-
import { useNavigate } from "react-router-dom";
26+
import { useNavigate, useLocation } from "react-router-dom";
2727
import Logo from '../../Icons/svg/interlex_logo.svg'
2828
import ListItemText from '@mui/material/ListItemText';
2929
import ListItemIcon from '@mui/material/ListItemIcon';
@@ -195,6 +195,7 @@ const Header = () => {
195195
setUserData(user, organization);
196196
};
197197
const navigate = useNavigate();
198+
const location = useLocation();
198199

199200
const handleClick = (event) => {
200201
setAnchorEl(event.currentTarget);
@@ -233,16 +234,6 @@ const Header = () => {
233234
const openUser = Boolean(anchorElUser);
234235
const idUser = open ? 'simple-popover' : undefined;
235236

236-
const [openList, setOpenList] = React.useState(false);
237-
238-
const handleCloseList = () => {
239-
setOpenList(false);
240-
};
241-
242-
const toggleList = () => {
243-
setOpenList(!openList);
244-
};
245-
246237
const handleMenuClick = async (e, menu) => {
247238
// Close both popovers
248239
handleClose();
@@ -267,24 +258,6 @@ const Header = () => {
267258
}
268259
}
269260

270-
React.useEffect(() => {
271-
const handleKeyDown = (event) => {
272-
if (event.ctrlKey && event.key === 'k') {
273-
toggleList();
274-
}
275-
if (event.key === 'Escape') {
276-
handleCloseList();
277-
}
278-
};
279-
280-
document.addEventListener('keydown', handleKeyDown);
281-
282-
return () => {
283-
document.removeEventListener('keydown', handleKeyDown);
284-
};
285-
// eslint-disable-next-line react-hooks/exhaustive-deps
286-
}, []);
287-
288261
React.useEffect(() => {
289262
console.log("Stored user in context ", user)
290263
if (user !== null && user?.groupname !== undefined) {
@@ -383,8 +356,8 @@ const Header = () => {
383356
{!isLoggedIn ? (
384357
<Box display='flex' gap='1.25rem'>
385358
<Box display='flex' gap='0.25rem'>
386-
<Button onClick={() => navigate("/register")}>Register</Button>
387-
<Button variant="outlined" onClick={() => navigate("/login")}>Log in</Button>
359+
<Button onClick={() => navigate("/register", { state: { from: location.pathname + location.search } })}>Register</Button>
360+
<Button variant="outlined" onClick={() => navigate("/login", { state: { from: location.pathname + location.search } })}>Log in</Button>
388361
</Box>
389362
<Divider sx={styles.divider} />
390363
<CustomButtonGroup options={options} disabled={!isLoggedIn} disabledTooltip="Log in to add terms or ontologies" />

src/components/SearchResults/ListView.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const TitleSection = ({ searchResult, onAddToActiveOntology }) => {
5959
const Description = ({ description }) => {
6060
return (
6161
<Typography variant='body2' sx={{ color: gray500 }}>
62-
{description === '' ? '-' : description}
62+
{description || '-'}
6363
</Typography>
6464
);
6565
};
@@ -200,7 +200,7 @@ const ListView = ({ searchResults, loading }) => {
200200
<TitleSection searchResult={searchResult} onAddToActiveOntology={handleAddToActiveOntology} />
201201
</Grid>
202202
<Grid item lg={12} xs={12} mt={2}>
203-
<Description description={searchResult.description} />
203+
<Description description={searchResult.definition} />
204204
</Grid>
205205
<Grid item lg={12} xs={12} sm={12} mt={3}>
206206
<InfoSection searchResult={searchResult} />

src/components/SearchResults/SearchResultsBox.jsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { TableChartIcon, ListIcon } from '../../Icons';
55
import OntologySearch from '../SingleTermView/OntologySearch';
66
import CustomSingleSelect from '../common/CustomSingleSelect';
77
import CustomViewButton from '../common/CustomViewButton';
8-
import { Box, Typography, Grid, ButtonGroup, Stack, Divider } from '@mui/material';
8+
import { Box, Typography, Grid, ButtonGroup, Stack, Divider, CircularProgress } from '@mui/material';
99
import CustomPagination from '../common/CustomPagination';
1010
import { vars } from '../../theme/variables';
1111
import { GlobalDataContext } from '../../contexts/DataContext';
@@ -45,7 +45,6 @@ const getPaginationSettings = (totalItems) => {
4545
};
4646

4747
const SearchResultsBox = ({
48-
allResults,
4948
pageResults,
5049
searchTerm,
5150
loading,
@@ -99,8 +98,12 @@ const SearchResultsBox = ({
9998
<Box width={1} flex={1} display="flex" flexDirection="column" px={4} py={3} gap={3} sx={{ overflowY: 'auto' }}>
10099
<Grid container justifyContent={{ lg: 'space-between', xs: 'flex-end', md: 'flex-end' }} alignItems="center">
101100
<Grid item xs={12} lg={6} sm={6}>
102-
<Typography variant="h5">
103-
{allResults.length} results for {searchTerm} search
101+
<Typography variant="h5" sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
102+
{loading ? (
103+
<CircularProgress size={20} />
104+
) : (
105+
`${totalItems} results for ${searchTerm} search`
106+
)}
104107
</Typography>
105108
</Grid>
106109
<Grid item xs={12} lg={6} sm={6}>
@@ -154,7 +157,6 @@ const SearchResultsBox = ({
154157
};
155158

156159
SearchResultsBox.propTypes = {
157-
allResults: PropTypes.object,
158160
pageResults: PropTypes.object,
159161
searchTerm: PropTypes.string,
160162
loading: PropTypes.bool,

0 commit comments

Comments
 (0)