From 6202964fcb8debf601d944b98693619bb36f1eea Mon Sep 17 00:00:00 2001 From: Dario Del Piano Date: Wed, 1 Jul 2026 01:17:32 +0200 Subject: [PATCH 1/5] GH issues 170, 168 --- src/App.jsx | 2 ++ src/components/Auth/Register.jsx | 5 +++-- src/components/Header/index.jsx | 7 ++++--- .../SingleOrganization/AddNewOntologyDialog.jsx | 10 ++++++---- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index d836f73..98a9270 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,6 +5,7 @@ import { BrowserRouter as Router, Routes, Route, + Navigate, useLocation, useNavigate } from "react-router-dom"; @@ -223,6 +224,7 @@ function MainContent() { } /> + } /> diff --git a/src/components/Auth/Register.jsx b/src/components/Auth/Register.jsx index 5c35ca3..e75dbee 100644 --- a/src/components/Auth/Register.jsx +++ b/src/components/Auth/Register.jsx @@ -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; @@ -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 diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index a9595b5..cdbcac7 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -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'; @@ -195,6 +195,7 @@ const Header = () => { setUserData(user, organization); }; const navigate = useNavigate(); + const location = useLocation(); const handleClick = (event) => { setAnchorEl(event.currentTarget); @@ -383,8 +384,8 @@ const Header = () => { {!isLoggedIn ? ( - - + + diff --git a/src/components/SingleOrganization/AddNewOntologyDialog.jsx b/src/components/SingleOrganization/AddNewOntologyDialog.jsx index 0f730e8..afe39ed 100644 --- a/src/components/SingleOrganization/AddNewOntologyDialog.jsx +++ b/src/components/SingleOrganization/AddNewOntologyDialog.jsx @@ -13,13 +13,13 @@ import { API_CONFIG } from "../../config"; import { GlobalDataContext } from "../../contexts/DataContext"; import { useContext } from "react"; -const HeaderRightSideContent = ({ handleClose, onAddNewOntology, disabled }) => { +const HeaderRightSideContent = ({ handleClose, onAddNewOntology, disabled, ready }) => { return ( ) @@ -28,7 +28,8 @@ const HeaderRightSideContent = ({ handleClose, onAddNewOntology, disabled }) => HeaderRightSideContent.propTypes = { handleClose: PropTypes.func, onAddNewOntology: PropTypes.func, - disabled: PropTypes.bool + disabled: PropTypes.bool, + ready: PropTypes.bool } const AddNewOntologyDialog = ({ open, handleClose, onOntologyAdded, organizationName }) => { @@ -350,6 +351,7 @@ const AddNewOntologyDialog = ({ open, handleClose, onOntologyAdded, organization handleClose={handleDialogClose} onAddNewOntology={handleAddNewOntology} disabled={submitting || !newOntology.uri.trim()} + ready={!!newOntology.uri.trim()} /> } > From 22499966f98f28bdeedc706cb90410dcf6a0016a Mon Sep 17 00:00:00 2001 From: Dario Del Piano Date: Wed, 1 Jul 2026 01:20:37 +0200 Subject: [PATCH 2/5] GH issue 169 --- src/components/Header/Search.jsx | 8 +++++++- src/components/Header/index.jsx | 28 ---------------------------- 2 files changed, 7 insertions(+), 29 deletions(-) diff --git a/src/components/Header/Search.jsx b/src/components/Header/Search.jsx index 6084872..af66e1a 100644 --- a/src/components/Header/Search.jsx +++ b/src/components/Header/Search.jsx @@ -167,8 +167,10 @@ const Search = () => { }, []); 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(); @@ -364,6 +366,10 @@ const Search = () => { placeholder="Find something..." onChange={handleInputChange} onKeyDown={handleEnterKey} + inputProps={{ + ...params.inputProps, + id: 'interlex-search-input', + }} InputProps={{ ...params.InputProps, startAdornment: ( diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index cdbcac7..682e0b8 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -234,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(); @@ -268,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) { From cba1b22b5b7adb16bef163ff09a8ec6ee8deb30b Mon Sep 17 00:00:00 2001 From: Dario Del Piano Date: Wed, 1 Jul 2026 01:23:32 +0200 Subject: [PATCH 3/5] GH issue 163 --- src/components/Header/Search.jsx | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/components/Header/Search.jsx b/src/components/Header/Search.jsx index af66e1a..f284b13 100644 --- a/src/components/Header/Search.jsx +++ b/src/components/Header/Search.jsx @@ -10,6 +10,7 @@ import { Chip, List, ListItem, + LinearProgress, } from "@mui/material"; import { debounce } from 'lodash'; import PropTypes from 'prop-types'; @@ -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 @@ -155,6 +157,7 @@ const Search = () => { setTerms([]) setOntologies([]) setOrganizations([]) + setIsSearching(false); }; const escapeSearch = useCallback(() => { @@ -164,6 +167,7 @@ const Search = () => { setTerms([]) setOntologies([]) setOrganizations([]) + setIsSearching(false); }, []); const handleKeyDown = useCallback(event => { @@ -184,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(() => { @@ -210,6 +219,7 @@ const Search = () => { const ListboxComponent = forwardRef(function ListboxComponent(props, ref) { return ( <> + {isSearching && } {searchTerm && (<> { ), endAdornment: ( - {openList ? ( + {searchTerm ? ( { > - Esc + {openList && Esc} ) : ( Ctrl + K From 0a1490509857c0d69f96b10ed68ef529c4f5b2dc Mon Sep 17 00:00:00 2001 From: Dario Del Piano Date: Wed, 1 Jul 2026 01:35:12 +0200 Subject: [PATCH 4/5] GH issue 165 --- .../SingleTermView/OverView/OverView.jsx | 16 ++- .../OverView/OverviewSideNav.jsx | 121 ++++++++++++++++++ 2 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 src/components/SingleTermView/OverView/OverviewSideNav.jsx diff --git a/src/components/SingleTermView/OverView/OverView.jsx b/src/components/SingleTermView/OverView/OverView.jsx index 028b27a..1b10b8b 100644 --- a/src/components/SingleTermView/OverView/OverView.jsx +++ b/src/components/SingleTermView/OverView/OverView.jsx @@ -28,6 +28,7 @@ import { } from "../../../parsers/hierarchies-parser"; import { createOverviewStore } from "./overviewStore"; import { DetailsSection, HierarchySection, PredicatesSection } from "./OverviewSections"; +import OverviewSideNav from "./OverviewSideNav"; import { emitPredicateRowUpdate, makeRowKey } from "./predicateMutationBus"; import { reportApiError } from "../../../api/apiErrorBus"; import ApiErrorDialog from "../../common/ApiErrorDialog"; @@ -39,6 +40,12 @@ const DETAILS_MIN_HEIGHT = 240; const HIERARCHY_MIN_HEIGHT = 420; const PREDICATES_MIN_HEIGHT = 320; +const SIDE_NAV_ITEMS = [ + { id: "overview-section-details", label: "Details" }, + { id: "overview-section-hierarchy", label: "Hierarchy & relations" }, + { id: "overview-section-predicates", label: "Predicates" }, +]; + const META_TITLES = new Set(["isabout", "ilx.isabout", "owl:versioniri"]); const norm = (t) => String(t || "").trim().toLowerCase(); @@ -484,11 +491,14 @@ const OverView = ({ searchTerm, isCodeViewVisible = false, selectedDataFormat, g ) : ( <> - + + + + - + - + { + const [activeId, setActiveId] = useState(items[0]?.id); + + useEffect(() => { + const elements = items + .map(({ id }) => document.getElementById(id)) + .filter(Boolean); + if (!elements.length) return undefined; + + const observer = new IntersectionObserver( + (entries) => { + const visible = entries + .filter((entry) => entry.isIntersecting) + .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0]; + if (visible) setActiveId(visible.target.id); + }, + { rootMargin: "-15% 0px -70% 0px", threshold: [0, 0.25, 0.5, 0.75, 1] } + ); + + elements.forEach((el) => observer.observe(el)); + return () => observer.disconnect(); + }, [items]); + + const handleClick = (id) => { + document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" }); + }; + + return ( + + {items.map(({ id, label }) => ( + handleClick(id)} + sx={{ + display: "flex", + alignItems: "center", + justifyContent: "flex-end", + gap: "0.5rem", + p: 0, + border: "none", + background: "none", + cursor: "pointer", + "&:hover .overview-side-nav-dot": { + transform: "scale(1.6)", + backgroundColor: activeId === id ? brand600 : gray600, + }, + "&:hover .overview-side-nav-label": { + opacity: 1, + transform: "translateX(0)", + }, + }} + > + + {label} + + + + ))} + + ); +}; + +OverviewSideNav.propTypes = { + items: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + }) + ).isRequired, +}; + +export default OverviewSideNav; From bf2b314715e1f978dc48a8b548ff448019325982 Mon Sep 17 00:00:00 2001 From: Dario Del Piano Date: Wed, 1 Jul 2026 01:58:03 +0200 Subject: [PATCH 5/5] GH issue 173 --- src/api/endpoints/index.ts | 20 +++++++++++ src/components/CurieEditor/CuriesTabPanel.jsx | 34 +++++++++++++++++-- src/components/CurieEditor/index.jsx | 7 ++-- src/components/SearchResults/ListView.jsx | 4 +-- .../SearchResults/SearchResultsBox.jsx | 12 ++++--- src/components/SearchResults/index.jsx | 3 +- 6 files changed, 67 insertions(+), 13 deletions(-) diff --git a/src/api/endpoints/index.ts b/src/api/endpoints/index.ts index 96478b9..f33e9ef 100644 --- a/src/api/endpoints/index.ts +++ b/src/api/endpoints/index.ts @@ -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; @@ -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); @@ -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 }; } } @@ -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 }; } }; diff --git a/src/components/CurieEditor/CuriesTabPanel.jsx b/src/components/CurieEditor/CuriesTabPanel.jsx index 27e5a49..c7fd6f0 100644 --- a/src/components/CurieEditor/CuriesTabPanel.jsx +++ b/src/components/CurieEditor/CuriesTabPanel.jsx @@ -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) diff --git a/src/components/CurieEditor/index.jsx b/src/components/CurieEditor/index.jsx index c8215c0..cd74eb2 100644 --- a/src/components/CurieEditor/index.jsx +++ b/src/components/CurieEditor/index.jsx @@ -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); @@ -126,7 +130,6 @@ const CurieEditor = () => { loading={curiesLoading} editMode={tab === 'base'} rows={localCuries[tab]} - onCurieAmountChange={handleCurieAmountChange} onAddRow={handleAddNewCurieRow} onDeleteRow={handleDeleteCurieRow} onChangeRow={handleInputChangeCurieRow} diff --git a/src/components/SearchResults/ListView.jsx b/src/components/SearchResults/ListView.jsx index c210013..014f312 100644 --- a/src/components/SearchResults/ListView.jsx +++ b/src/components/SearchResults/ListView.jsx @@ -59,7 +59,7 @@ const TitleSection = ({ searchResult, onAddToActiveOntology }) => { const Description = ({ description }) => { return ( - {description === '' ? '-' : description} + {description || '-'} ); }; @@ -200,7 +200,7 @@ const ListView = ({ searchResults, loading }) => { - + diff --git a/src/components/SearchResults/SearchResultsBox.jsx b/src/components/SearchResults/SearchResultsBox.jsx index f6d05d0..1cd7a07 100644 --- a/src/components/SearchResults/SearchResultsBox.jsx +++ b/src/components/SearchResults/SearchResultsBox.jsx @@ -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'; @@ -45,7 +45,6 @@ const getPaginationSettings = (totalItems) => { }; const SearchResultsBox = ({ - allResults, pageResults, searchTerm, loading, @@ -99,8 +98,12 @@ const SearchResultsBox = ({ - - {allResults.length} results for {searchTerm} search + + {loading ? ( + + ) : ( + `${totalItems} results for ${searchTerm} search` + )} @@ -154,7 +157,6 @@ const SearchResultsBox = ({ }; SearchResultsBox.propTypes = { - allResults: PropTypes.object, pageResults: PropTypes.object, searchTerm: PropTypes.string, loading: PropTypes.bool, diff --git a/src/components/SearchResults/index.jsx b/src/components/SearchResults/index.jsx index 604b762..ff7d50c 100644 --- a/src/components/SearchResults/index.jsx +++ b/src/components/SearchResults/index.jsx @@ -4,6 +4,7 @@ import { debounce } from 'lodash'; import { useQuery } from '../../helpers'; import SearchResultsBox from './SearchResultsBox'; import FiltersSidebar from '../Sidebar/FiltersSidebar'; +import ApiErrorDialog from '../common/ApiErrorDialog'; import { elasticSearch } from '../../api/endpoints'; const SearchResults = () => { @@ -98,13 +99,13 @@ const SearchResults = () => { return ( <> +