Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
BrowserRouter as Router,
Routes,
Route,
Navigate,
useLocation,
useNavigate
} from "react-router-dom";
Expand Down Expand Up @@ -223,6 +224,7 @@ function MainContent() {
</PageContainer>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Layout>
</Box>
Expand Down
20 changes: 20 additions & 0 deletions src/api/endpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import curieParser from '../../parsers/curieParser';
import termParser, { elasticSearchParser, getTerm } from '../../parsers/termParser';
import axios from 'axios';
import { API_CONFIG } from '../../config';
import { reportApiError } from '../apiErrorBus';

const useApi = () => api;
const useMockApi = () => mockApi;
Expand Down Expand Up @@ -148,6 +149,13 @@ const fetchData = async (url, method = "GET", data: object | null = null) => {
},
withCredentials : true
});
// Some backends (e.g. the Elasticsearch proxy) return HTTP 200 with
// the failure encoded in the body, so axios never rejects on its own.
if (response.data?.error) {
const bodyError: any = new Error(response.data.error.message || `Request failed with code ${response.data.error.code}`);
bodyError.status = response.data.error.code;
throw bodyError;
}
return response.data;
} catch (error) {
console.error(`API Error at ${url}:`, error);
Expand Down Expand Up @@ -175,6 +183,12 @@ export const elasticSearch = async (
total = initialResponse?.hits?.total ?? 0;
} catch (error) {
console.error("Failed to fetch total count from Elasticsearch:", error);
reportApiError({
context: `Search "${query}"`,
url,
status: error?.status ?? error?.response?.status,
message: error?.message || "Request failed",
});
return { results: [], total: 0 };
}
}
Expand All @@ -192,6 +206,12 @@ export const elasticSearch = async (
};
} catch (error) {
console.error("Error when performing elastic search", error);
reportApiError({
context: `Search "${query}"`,
url,
status: error?.status ?? error?.response?.status,
message: error?.message || "Request failed",
});
return { results: [], total: 0 };
}
};
Expand Down
5 changes: 3 additions & 2 deletions src/components/Auth/Register.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { API_CONFIG } from "../../config";
import { useCookies } from 'react-cookie';
import PasswordField from "./UI/PasswordField";
import { ArrowBack } from "@mui/icons-material";
import { Link, useNavigate } from "react-router-dom";
import { Link, useNavigate, useLocation } from "react-router-dom";
// import { GlobalDataContext } from "../../contexts/DataContext";

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

React.useEffect(() => {
if (prevSnackbarOpen.current && !snackbarOpen) {
closePopups();
navigate("/login");
navigate("/login", { state: { from: location.state?.from } });
}
prevSnackbarOpen.current = snackbarOpen;
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
34 changes: 31 additions & 3 deletions src/components/CurieEditor/CuriesTabPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,39 @@ const CuriesTabPanel = (props) => {
const [columnIndex, setColumnIndex] = React.useState(-1);
const [order, setOrder] = React.useState('asc');
const [orderBy, setOrderBy] = React.useState('prefix');
const [displayOrder, setDisplayOrder] = React.useState([]);
const isEditing = rowId !== null;

const sortedRows = React.useMemo(() => {
const sortIds = (rowsArr) => stableSort(rowsArr, getComparator(order, orderBy)).map((row) => row._id);

// Manual header sort always re-orders, even mid-edit.
React.useEffect(() => {
setDisplayOrder(sortIds(Array.isArray(rows) ? rows : []));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [order, orderBy]);

// Row content changes (typing, add/delete) only trigger a full re-sort once
// editing is done; while editing, just reconcile which rows exist so a row
// doesn't jump position under the user as soon as it gets a value.
React.useEffect(() => {
const safeRows = Array.isArray(rows) ? rows : [];
return stableSort(safeRows, getComparator(order, orderBy));
}, [rows, order, orderBy]);
if (!isEditing) {
setDisplayOrder(sortIds(safeRows));
return;
}
const currentIds = safeRows.map((row) => row._id);
setDisplayOrder((prevOrder) => {
const stillPresent = prevOrder.filter((id) => currentIds.includes(id));
const newIds = currentIds.filter((id) => !prevOrder.includes(id));
return [...stillPresent, ...newIds];
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rows, isEditing]);

const sortedRows = React.useMemo(() => {
const rowsById = new Map((Array.isArray(rows) ? rows : []).map((row) => [row._id, row]));
return displayOrder.map((id) => rowsById.get(id)).filter(Boolean);
}, [displayOrder, rows]);

React.useEffect(() => {
onCurieAmountChange?.(rows.length)
Expand Down
7 changes: 5 additions & 2 deletions src/components/CurieEditor/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ const CurieEditor = () => {
setOpenCurieEditor(true);
};

const handleCloseCurieEditor = () => setOpenCurieEditor(false);
const handleCloseCurieEditor = () => {
// Discard any unsaved edits/added rows made in the dialog.
setLocalCuries(curies);
setOpenCurieEditor(false);
};
const handleChangeTabs = (event, newValue) => setTabValue(newValue);
const handleCurieAmountChange = (value) => setCurieAmount(value);

Expand Down Expand Up @@ -126,7 +130,6 @@ const CurieEditor = () => {
loading={curiesLoading}
editMode={tab === 'base'}
rows={localCuries[tab]}
onCurieAmountChange={handleCurieAmountChange}
onAddRow={handleAddNewCurieRow}
onDeleteRow={handleDeleteCurieRow}
onChangeRow={handleInputChangeCurieRow}
Expand Down
36 changes: 26 additions & 10 deletions src/components/Header/Search.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Chip,
List,
ListItem,
LinearProgress,
} from "@mui/material";
import { debounce } from 'lodash';
import PropTypes from 'prop-types';
Expand Down Expand Up @@ -91,6 +92,7 @@ const Search = () => {
const [terms, setTerms] = useState([]);
const [organizations, setOrganizations] = useState([]);
const [ontologies, setOntologies] = useState([]);
const [isSearching, setIsSearching] = useState(false);
const { storedSearchTerm, updateStoredSearchTerm, user } = useContext(GlobalDataContext);

// Get the group name based on user login status
Expand Down Expand Up @@ -155,6 +157,7 @@ const Search = () => {
setTerms([])
setOntologies([])
setOrganizations([])
setIsSearching(false);
};

const escapeSearch = useCallback(() => {
Expand All @@ -164,11 +167,14 @@ const Search = () => {
setTerms([])
setOntologies([])
setOrganizations([])
setIsSearching(false);
}, []);

const handleKeyDown = useCallback(event => {
if (event.ctrlKey && event.key === 'k') {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
event.preventDefault();
setOpenList(true);
document.getElementById('interlex-search-input')?.focus();
}
if (event.key === 'Escape') {
escapeSearch();
Expand All @@ -182,13 +188,18 @@ const Search = () => {

// eslint-disable-next-line react-hooks/exhaustive-deps
const fetchTerms = useCallback(debounce(async (searchTerm) => {
const data = await elasticSearch(searchTerm, 20, 0);
const dataTerms = data?.results.results?.filter(result => result.type === SEARCH_TYPES.TERM);
const dataOrganizations = data?.results.results?.filter(result => result.type === SEARCH_TYPES.ORGANIZATION);
const dataOntologies = data?.results.results?.filter(result => result.type === SEARCH_TYPES.ONTOLOGY);
setTerms(dataTerms);
setOrganizations(dataOrganizations);
setOntologies(dataOntologies);
setIsSearching(true);
try {
const data = await elasticSearch(searchTerm, 20, 0);
const dataTerms = data?.results.results?.filter(result => result.type === SEARCH_TYPES.TERM);
const dataOrganizations = data?.results.results?.filter(result => result.type === SEARCH_TYPES.ORGANIZATION);
const dataOntologies = data?.results.results?.filter(result => result.type === SEARCH_TYPES.ONTOLOGY);
setTerms(dataTerms);
setOrganizations(dataOrganizations);
setOntologies(dataOntologies);
} finally {
setIsSearching(false);
}
}, 500), [searchAll]);

useEffect(() => {
Expand All @@ -208,6 +219,7 @@ const Search = () => {
const ListboxComponent = forwardRef(function ListboxComponent(props, ref) {
return (
<>
{isSearching && <LinearProgress sx={{ height: '0.125rem' }} />}
{searchTerm && (<><Box p="0.5rem">
<List sx={{
'& .MuiTypography-body1': {
Expand Down Expand Up @@ -364,6 +376,10 @@ const Search = () => {
placeholder="Find something..."
onChange={handleInputChange}
onKeyDown={handleEnterKey}
inputProps={{
...params.inputProps,
id: 'interlex-search-input',
}}
InputProps={{
...params.InputProps,
startAdornment: (
Expand All @@ -373,15 +389,15 @@ const Search = () => {
),
endAdornment: (
<InputAdornment position="end">
{openList ? (
{searchTerm ? (
<Box display="flex" alignItems="center" gap="0.75rem">
<IconButton
sx={styles.searchButton}
onClick={resetSearch}
>
<CloseIcon />
</IconButton>
<Box sx={styles.keyBoardInfo}>Esc</Box>
{openList && <Box sx={styles.keyBoardInfo}>Esc</Box>}
</Box>
) : (
<Box sx={styles.keyBoardInfo}>Ctrl + K</Box>
Expand Down
35 changes: 4 additions & 31 deletions src/components/Header/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import Search from './Search';
import { useContext } from "react";
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import { useNavigate } from "react-router-dom";
import { useNavigate, useLocation } from "react-router-dom";
import Logo from '../../Icons/svg/interlex_logo.svg'
import ListItemText from '@mui/material/ListItemText';
import ListItemIcon from '@mui/material/ListItemIcon';
Expand Down Expand Up @@ -195,6 +195,7 @@ const Header = () => {
setUserData(user, organization);
};
const navigate = useNavigate();
const location = useLocation();

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

const [openList, setOpenList] = React.useState(false);

const handleCloseList = () => {
setOpenList(false);
};

const toggleList = () => {
setOpenList(!openList);
};

const handleMenuClick = async (e, menu) => {
// Close both popovers
handleClose();
Expand All @@ -267,24 +258,6 @@ const Header = () => {
}
}

React.useEffect(() => {
const handleKeyDown = (event) => {
if (event.ctrlKey && event.key === 'k') {
toggleList();
}
if (event.key === 'Escape') {
handleCloseList();
}
};

document.addEventListener('keydown', handleKeyDown);

return () => {
document.removeEventListener('keydown', handleKeyDown);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

React.useEffect(() => {
console.log("Stored user in context ", user)
if (user !== null && user?.groupname !== undefined) {
Expand Down Expand Up @@ -383,8 +356,8 @@ const Header = () => {
{!isLoggedIn ? (
<Box display='flex' gap='1.25rem'>
<Box display='flex' gap='0.25rem'>
<Button onClick={() => navigate("/register")}>Register</Button>
<Button variant="outlined" onClick={() => navigate("/login")}>Log in</Button>
<Button onClick={() => navigate("/register", { state: { from: location.pathname + location.search } })}>Register</Button>
<Button variant="outlined" onClick={() => navigate("/login", { state: { from: location.pathname + location.search } })}>Log in</Button>
</Box>
<Divider sx={styles.divider} />
<CustomButtonGroup options={options} disabled={!isLoggedIn} disabledTooltip="Log in to add terms or ontologies" />
Expand Down
4 changes: 2 additions & 2 deletions src/components/SearchResults/ListView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const TitleSection = ({ searchResult, onAddToActiveOntology }) => {
const Description = ({ description }) => {
return (
<Typography variant='body2' sx={{ color: gray500 }}>
{description === '' ? '-' : description}
{description || '-'}
</Typography>
);
};
Expand Down Expand Up @@ -200,7 +200,7 @@ const ListView = ({ searchResults, loading }) => {
<TitleSection searchResult={searchResult} onAddToActiveOntology={handleAddToActiveOntology} />
</Grid>
<Grid item lg={12} xs={12} mt={2}>
<Description description={searchResult.description} />
<Description description={searchResult.definition} />
</Grid>
<Grid item lg={12} xs={12} sm={12} mt={3}>
<InfoSection searchResult={searchResult} />
Expand Down
12 changes: 7 additions & 5 deletions src/components/SearchResults/SearchResultsBox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { TableChartIcon, ListIcon } from '../../Icons';
import OntologySearch from '../SingleTermView/OntologySearch';
import CustomSingleSelect from '../common/CustomSingleSelect';
import CustomViewButton from '../common/CustomViewButton';
import { Box, Typography, Grid, ButtonGroup, Stack, Divider } from '@mui/material';
import { Box, Typography, Grid, ButtonGroup, Stack, Divider, CircularProgress } from '@mui/material';
import CustomPagination from '../common/CustomPagination';
import { vars } from '../../theme/variables';
import { GlobalDataContext } from '../../contexts/DataContext';
Expand Down Expand Up @@ -45,7 +45,6 @@ const getPaginationSettings = (totalItems) => {
};

const SearchResultsBox = ({
allResults,
pageResults,
searchTerm,
loading,
Expand Down Expand Up @@ -99,8 +98,12 @@ const SearchResultsBox = ({
<Box width={1} flex={1} display="flex" flexDirection="column" px={4} py={3} gap={3} sx={{ overflowY: 'auto' }}>
<Grid container justifyContent={{ lg: 'space-between', xs: 'flex-end', md: 'flex-end' }} alignItems="center">
<Grid item xs={12} lg={6} sm={6}>
<Typography variant="h5">
{allResults.length} results for {searchTerm} search
<Typography variant="h5" sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
{loading ? (
<CircularProgress size={20} />
) : (
`${totalItems} results for ${searchTerm} search`
)}
</Typography>
</Grid>
<Grid item xs={12} lg={6} sm={6}>
Expand Down Expand Up @@ -154,7 +157,6 @@ const SearchResultsBox = ({
};

SearchResultsBox.propTypes = {
allResults: PropTypes.object,
pageResults: PropTypes.object,
searchTerm: PropTypes.string,
loading: PropTypes.bool,
Expand Down
Loading
Loading