From bc1c6023f80290ca23f9eecd942bc31b71fd057c Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Thu, 24 Jul 2025 15:51:46 +0200 Subject: [PATCH 01/18] ILEX-99 add new term changes --- src/components/Header/index.jsx | 44 ++- src/components/TermEditor/TermDialog.jsx | 8 +- .../TermEditor/newTerm/AddNewTermDialog.jsx | 258 ++++++++++++++++++ .../TermEditor/newTerm/FirstStepContent.jsx | 139 ++++++++++ .../TermEditor/newTerm/SecondStepContent.jsx | 52 ++++ src/components/common/CustomButtonGroup.jsx | 140 ++++++++++ src/components/common/CustomizedInput.jsx | 109 +++++--- src/theme/index.jsx | 20 +- 8 files changed, 704 insertions(+), 66 deletions(-) create mode 100644 src/components/TermEditor/newTerm/AddNewTermDialog.jsx create mode 100644 src/components/TermEditor/newTerm/FirstStepContent.jsx create mode 100644 src/components/TermEditor/newTerm/SecondStepContent.jsx create mode 100644 src/components/common/CustomButtonGroup.jsx diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index e782d8b0..222dae41 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -1,5 +1,4 @@ import { - AddIcon, DocumentationIcon, LogoutIcon, NavIcon, @@ -25,7 +24,6 @@ import { useContext } from "react"; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import { useNavigate } from "react-router-dom"; -import TermDialog from '../TermEditor/TermDialog'; import Logo from '../../Icons/svg/interlex_logo.svg' import ListItemText from '@mui/material/ListItemText'; import ListItemIcon from '@mui/material/ListItemIcon'; @@ -36,6 +34,10 @@ import ModeEditOutlineOutlinedIcon from "@mui/icons-material/ModeEditOutlineOutl import PersonOutlineIcon from '@mui/icons-material/PersonOutline'; import { userLogout } from "../../api/endpoints/apiService"; import { useCookies } from 'react-cookie'; +import CustomButtonGroup from '../common/CustomButtonGroup'; +import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd'; +import AddIcon from '@mui/icons-material/Add'; +import AddNewTermDialog from '../TermEditor/newTerm/AddNewTermDialog'; import { vars } from "../../theme/variables"; const { gray200, white, gray100, gray600 } = vars; @@ -279,6 +281,19 @@ const Header = () => { !menu.protected || (menu.protected && isLoggedIn) ); + const options = [ + { + label: 'Add a new term', + icon: , + action: handleNewTermDialogOpen + }, + { + label: 'Bulk add terms', + icon: , + action: handleOpenEditBulkTerms + } + ]; + return ( <> @@ -350,17 +365,21 @@ const Header = () => { - + } + options={options} + onOptionSelect={(option) => console.log('Selected:', option.label)} + /> ) : ( - + } + options={options} + onOptionSelect={(option) => console.log('Selected:', option.label)} + /> { )} - + ) diff --git a/src/components/TermEditor/TermDialog.jsx b/src/components/TermEditor/TermDialog.jsx index bca961c7..4fae115a 100644 --- a/src/components/TermEditor/TermDialog.jsx +++ b/src/components/TermEditor/TermDialog.jsx @@ -20,13 +20,7 @@ const HeaderRightSideContent = ({ activeStep, onContinue, onClose, isContinueBut steps={3} position="static" activeStep={activeStep} - sx={{ - maxWidth: 64, - flexGrow: 1, - '& .MuiMobileStepper-dots': { gap: '0.75rem' }, - '& .MuiMobileStepper-dot': { margin: 0, backgroundColor: gray200 }, - '& .MuiMobileStepper-dotActive': { backgroundColor: brand700 } - }} + sx={{ maxWidth: 64, flexGrow: 1 }} /> diff --git a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx new file mode 100644 index 00000000..8bc3836e --- /dev/null +++ b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx @@ -0,0 +1,258 @@ +import { useState, useContext, useMemo, useCallback, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import PropTypes from "prop-types"; +import { debounce } from 'lodash'; +import { + Box, + Divider, + MobileStepper, + Stack, + Button, + FormControlLabel, + Checkbox +} from "@mui/material"; +import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; +import CustomButton from '../../common/CustomButton'; +import CustomizedDialog from "../../common/CustomizedDialog"; +import OntologySearch from '../../SingleTermView/OntologySearch'; +import FirstStepContent from "./FirstStepContent"; +import SecondStepContent from "./SecondStepContent"; +import StatusStep from "../../common/StatusStep"; +import { GlobalDataContext } from '../../../contexts/DataContext'; +import { getAddTermStatusProps } from '../termStatusProps'; +import { getEndpointsIlx, elasticSearch } from '../../../api/endpoints/index'; +import { CheckedIcon, UncheckedIcon } from '../../../Icons'; +import { vars } from "../../../theme/variables"; + +const { gray100, gray200, gray400 } = vars; + +const INITIAL_FORM_STATE = { + label: "", + synonyms: [], + superClass: "", + existingIDs: [], + isDefinedBy: "", + description: "", + comment: "" +}; + +const HeaderRightSideContent = ({ + activeStep, + onContinue, + onClose, + isContinueButtonDisabled +}) => ( + + {activeStep !== 2 ? ( + <> + } + checkedIcon={} + /> + } + sx={{ color: "#515252" }} + label="Add to ontology" + /> + + + + + + Cancel + + + + ) : ( + + )} + +); + +HeaderRightSideContent.propTypes = { + activeStep: PropTypes.number.isRequired, + onContinue: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + isContinueButtonDisabled: PropTypes.bool.isRequired +}; + +const AddNewTermDialog = ({ + open, + handleClose, + searchTerm, + forwardPredicateStep +}) => { + const [activeStep, setActiveStep] = useState(0); + const [areMatchesChecked, setAreMatchesChecked] = useState(false); + const [openSidebar, setOpenSidebar] = useState(true); + const [loading, setLoading] = useState(true); + const [termResults, setTermResults] = useState([]); + const [addTermResponse, setAddTermResponse] = useState(null); + const [termValue, setTermValue] = useState(''); + const [formState, setFormState] = useState(INITIAL_FORM_STATE); + const [newTermId, setNewTermId] = useState(""); + const [selectedType, setSelectedType] = useState(null); + + const navigate = useNavigate(); + const { user } = useContext(GlobalDataContext); + const [data] = useState(null); + const memoData = useMemo(() => data, [data]); + + const isSearchTermAvailable = Boolean(searchTerm); + const isContinueButtonDisabled = !isSearchTermAvailable && !areMatchesChecked; + const isResultsEmpty = termResults?.length === 0; + const statusProps = getAddTermStatusProps(addTermResponse, termValue); + + const handleContinueClick = () => setActiveStep(activeStep + 1); + + const handleCancelBtnClick = () => { + handleClose(); + setActiveStep(0); + setAreMatchesChecked(false); + }; + + const handleSidebarToggle = () => setOpenSidebar(!openSidebar); + + + const handleGoToTermClick = () => { + const groupName = user?.groupname || 'base'; + navigate(`/${groupName}/${newTermId}/overview`); + onClose(); + }; + + const handleTypeChange = (newType) => { + setSelectedType(newType) + } + + + const fetchTerms = useCallback( + debounce((termValue) => { + setLoading(true); + elasticSearch(termValue).then(data => { + setTermResults(data.results?.results); + setLoading(false); + }); + }, 300), + [getEndpointsIlx, elasticSearch] + ); + + const addTermRequest = useCallback(async (group, term) => { + const token = localStorage.getItem("token"); + const groupName = user?.groupname || group; + const body = { + 'rdf-type': term.superClass || 'owl:Class', + label: term.label, + exact: term.synonyms?.map(s => s.label), + }; + + try { + const response = await createNewEntity({ + group: groupName, + data: body, + session: token + }); + setAddTermResponse(response); + setNewTermId(response.term.id.split("/").pop()); + } catch (error) { + setAddTermResponse(error); + } + }, [user]); + + + useEffect(() => { + elasticSearch(termValue, 20, 0).then(data => { + setTermResults(data.results?.results); + setLoading(false); + }); + }, [termValue]); + + useEffect(() => { + fetchTerms(termValue); + return () => { + fetchTerms.cancel(); + }; + }, [termValue, fetchTerms]); + + useEffect(() => { + if (memoData?.predicates) { + setPredicates(memoData.predicates); + } + }, [memoData]); + + useEffect(() => { + if (activeStep === 2) { + addTermRequest("base", formState); + } + }, [addTermRequest, formState, activeStep]); + + useEffect(() => { + if (forwardPredicateStep) { + setActiveStep(1); + } + }, [forwardPredicateStep]); + + return ( + + } + sx={{ '& .MuiDialogContent-root': { padding: 0, overflowY: "hidden" } }} + > + {activeStep === 0 && ( + + )} + {activeStep === 1 && } + {activeStep === 2 && addTermResponse != null && ( + + )} + + ); +}; + +AddNewTermDialog.propTypes = { + open: PropTypes.bool.isRequired, + handleClose: PropTypes.func.isRequired, + searchTerm: PropTypes.string, + forwardPredicateStep: PropTypes.bool +}; + +export default AddNewTermDialog; diff --git a/src/components/TermEditor/newTerm/FirstStepContent.jsx b/src/components/TermEditor/newTerm/FirstStepContent.jsx new file mode 100644 index 00000000..e7192be4 --- /dev/null +++ b/src/components/TermEditor/newTerm/FirstStepContent.jsx @@ -0,0 +1,139 @@ +import { useState } from "react"; +import { Box, Divider, Stack, Typography, Autocomplete, Chip, TextField } from "@mui/material"; +import CustomSingleSelect from "../../common/CustomSingleSelect"; +import CustomizedInput from "../../common/CustomizedInput"; +import NewTermSidebar from "../NewTermSidebar"; +import { HelpOutlinedIcon } from "../../../Icons"; + +const TYPES = ["Term", "Relationship", "Ontology"] + +const FirstStepContent = ({ + openSidebar, + loading, + onToggle, + termResults, + isResultsEmpty, + selectedType, + onTypeChange +}) => { + + const [value, setValue] = useState([top100Films[0]]); + + return ( + + + + How would you like to proceed? + + Link this term to an existing source, either by reusing an existing ID or by specifying an exact synonym. + + + + + Exact synonyms + + Synonyms are especially useful when the term is known by different names. + Please enter the type, label and exact synonym(s) for your object. + + + + + } + /> + + { + setValue([ + ...newValue, + ]); + }} + popupIcon={} + options={top100Films} + getOptionLabel={(option) => option.title} + renderValue={(values, getItemProps) => + values.map((option, index) => { + const { key, ...itemProps } = getItemProps({ index }); + return ( + + ); + }) + } + fullWidth + renderInput={(params) => ( + + )} + sx={{ + '& .MuiOutlinedInput-root': { + background: "#fff", + borderColor: "#BDC2C1" + }, + '& .MuiOutlinedInput-root .MuiAutocomplete-input': { + color: "#707574", + fontWeight: 400 + }, + '& .MuiAutocomplete-popupIndicator': { + transform: 'none !important', + } + }} + /> + + + + + Existing IDs + + Synonyms are especially useful when the term is known by different names. + + + } + /> + + + + + ) +}; + +export default FirstStepContent; + +const top100Films = [ + { title: 'The Shawshank Redemption', year: 1994 }, + { title: 'The Godfather', year: 1972 }, + { title: 'The Godfather: Part II', year: 1974 }, + { title: 'The Dark Knight', year: 2008 }, + { title: '12 Angry Men', year: 1957 }, + { title: "Schindler's List", year: 1993 }, + { title: 'Pulp Fiction', year: 1994 } +] \ No newline at end of file diff --git a/src/components/TermEditor/newTerm/SecondStepContent.jsx b/src/components/TermEditor/newTerm/SecondStepContent.jsx new file mode 100644 index 00000000..342a039b --- /dev/null +++ b/src/components/TermEditor/newTerm/SecondStepContent.jsx @@ -0,0 +1,52 @@ +import { Box, Grid, Typography } from "@mui/material"; +import CustomInputBox from "../../common/CustomInputBox"; + +const SecondStepContent = () => { + return ( + + + + Term metadata overview + + + + + + + + + + + + + ) +} + +export default SecondStepContent; \ No newline at end of file diff --git a/src/components/common/CustomButtonGroup.jsx b/src/components/common/CustomButtonGroup.jsx new file mode 100644 index 00000000..b4f3b3a2 --- /dev/null +++ b/src/components/common/CustomButtonGroup.jsx @@ -0,0 +1,140 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import Grow from '@mui/material/Grow'; +import Paper from '@mui/material/Paper'; +import Popper from '@mui/material/Popper'; +import MenuItem from '@mui/material/MenuItem'; +import MenuList from '@mui/material/MenuList'; +import { KeyboardArrowUp, KeyboardArrowDown } from "@mui/icons-material"; +import { vars } from '../../theme/variables'; + +const { white, gray200, gray600, paperShadow } = vars; + +const styles = { + paper: { + borderRadius: "0.5rem", + border: `1px solid ${gray200}`, + backgroundColor: white, + boxShadow: paperShadow + }, + menu: { + padding: "0.25rem 0" + }, + menuItem: { + padding: "0.625rem 0.875rem", + fontSize: "0.875rem", + fontWeight: 600, + lineHeight: "1.25rem", + color: gray600, + gap: "0.25rem" + } +} + +const CustomButtonGroup = ({ + buttonTitle, + buttonIcon = null, + options = [], + onOptionSelect +}) => { + const [open, setOpen] = React.useState(false); + const anchorRef = React.useRef(null); + + const handleMainButtonClick = () => { + if (options.length === 0 && onOptionSelect) { + onOptionSelect(null); + } + }; + + const handleMenuItemClick = (event, option) => { + setOpen(false); + if (option.action) { + option.action(); + } else if (onOptionSelect) { + onOptionSelect(option); + } + }; + + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const handleClose = (event) => { + if (anchorRef.current && anchorRef.current.contains(event.target)) { + return; + } + setOpen(false); + }; + + return ( + + + + {options.length > 0 && ( + + )} + + {options.length > 0 && ( + + {({ TransitionProps, placement }) => ( + + + + + {options.map((option, index) => ( + handleMenuItemClick(event, option)} + sx={styles.menuItem} + > + {option.icon} + {option.label} + + ))} + + + + + )} + + )} + + ); +} + +export default CustomButtonGroup; \ No newline at end of file diff --git a/src/components/common/CustomizedInput.jsx b/src/components/common/CustomizedInput.jsx index 3dc16359..9f1dd745 100644 --- a/src/components/common/CustomizedInput.jsx +++ b/src/components/common/CustomizedInput.jsx @@ -1,74 +1,99 @@ import PropTypes from 'prop-types'; -import {Typography} from "@mui/material"; +import { Typography, InputAdornment } from "@mui/material"; import { styled } from '@mui/material/styles'; import InputBase from '@mui/material/InputBase'; import FormControl from '@mui/material/FormControl'; -import {vars} from "../../theme/variables"; -const { gray800, gray300, gray700, brand600, gray500, gray50 }= vars +import { vars } from "../../theme/variables"; +const { gray800, gray300, gray700, brand600, gray500, gray50 } = vars; -const BootstrapInput = styled(InputBase)(() => ({ - '& .MuiInputBase-input': { - borderRadius: '.5rem', +const BootstrapInput = styled(InputBase)(({ theme }) => ({ + "& .MuiInputBase-input": { + borderRadius: ".5rem", border: `1px solid ${gray300}`, - fontSize: '1rem', - width: '100%', - padding: '.5rem .575rem', - height: '2.5rem', + fontSize: "1rem", + width: "100%", + padding: ".5rem .75rem", + height: "2.5rem", color: gray700, - '&:focus': { + "&:focus": { borderColor: brand600, - boxShadow: 'none', - borderWidth: '2px' + boxShadow: "none", + borderWidth: "2px", }, - '&::placeholder': { + "&::placeholder": { color: gray500, - fontSize: '1rem', + fontSize: "1rem", }, - '&.Mui-disabled': { + "&.Mui-disabled": { backgroundColor: gray50, fontWeight: 400, - opacity: 1 - } + opacity: 1, + }, + }, + "& .MuiInputBase-root": { + position: "relative", + }, + "& .MuiInputAdornment-root": { + position: "absolute", + right: ".75rem", + top: "50%", + transform: "translateY(-50%)", + pointerEvents: "auto", }, + "&.MuiInputBase-adornedEnd .MuiInputBase-input": { + paddingRight: "2.5rem" + } })); + const CustomizedInput = (props) => { - const {label, value, onChange, placeholder} = props + const { label, value, onChange, placeholder, endAdornment, ...otherProps } = props; + return ( <> - {label && - {label} - } + {label && ( + + {label} + + )} - + {endAdornment}} + {...otherProps} + /> ); -} +}; CustomizedInput.propTypes = { label: PropTypes.string, value: PropTypes.string, onChange: PropTypes.func, placeholder: PropTypes.string, - sx: PropTypes.object -} + sx: PropTypes.object, + endAdornment: PropTypes.node // For adding icons inside the input +}; -export default CustomizedInput; +export default CustomizedInput; \ No newline at end of file diff --git a/src/theme/index.jsx b/src/theme/index.jsx index 7d5fb139..4984e038 100644 --- a/src/theme/index.jsx +++ b/src/theme/index.jsx @@ -613,13 +613,16 @@ const theme = createTheme({ MuiButtonGroup: { styleOverrides: { - outlined: { + root: { borderRadius: "0.5rem", - boxShadow: "0px 1px 2px 0px rgba(16, 24, 40, 0.05)", "& .MuiButton-root": { padding: "0.625rem 0.875rem", - }, + } + }, + outlined: { + boxShadow: "0px 1px 2px 0px rgba(16, 24, 40, 0.05)", + "& .MuiButton-root:focus": { boxShadow: "none", background: gray50, @@ -628,6 +631,13 @@ const theme = createTheme({ borderRightColor: "transparent", }, }, + contained: { + boxShadow: "0px 1px 2px 0px rgba(16, 24, 40, 0.05)", + + "& .MuiButtonGroup-firstButton": { + borderColor: white, + } + } }, }, @@ -850,10 +860,14 @@ const theme = createTheme({ MuiMobileStepper: { styleOverrides: { + dots: { + gap: "0.75rem" + }, dot: { width: ".5rem", height: ".5rem", backgroundColor: gray200, + margin: 0 }, dotActive: { backgroundColor: brand700, From 594346bd394ecc53211e4431d27fe191cf4cc920 Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Thu, 31 Jul 2025 16:46:13 +0200 Subject: [PATCH 02/18] ILEX-99 make changes --- src/components/TermEditor/NewTermSidebar.jsx | 354 +++++++++++++----- .../TermEditor/newTerm/AddNewTermDialog.jsx | 131 +------ .../TermEditor/newTerm/FirstStepContent.jsx | 267 ++++++++----- .../TermEditor/newTerm/SecondStepContent.jsx | 280 ++++++++++++-- src/components/common/CustomButtonGroup.jsx | 19 +- 5 files changed, 709 insertions(+), 342 deletions(-) diff --git a/src/components/TermEditor/NewTermSidebar.jsx b/src/components/TermEditor/NewTermSidebar.jsx index 6deb79ef..3f4f3aea 100644 --- a/src/components/TermEditor/NewTermSidebar.jsx +++ b/src/components/TermEditor/NewTermSidebar.jsx @@ -1,101 +1,282 @@ import PropTypes from 'prop-types'; import { StartIcon, JoinRightIcon } from '../../Icons'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; -import { Box, Typography, IconButton, Tooltip, Stack, CircularProgress } from '@mui/material'; - +import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward'; +import { + Box, + Typography, + IconButton, + Tooltip, + Stack, + CircularProgress, + Chip, + Button +} from '@mui/material'; import { vars } from '../../theme/variables'; -const { gray200, gray800, brand600 } = vars; -export default function NewTermSidebar({ open, loading, onToggle, results, isResultsEmpty }) { - if (loading) { - return - - - } +const { + gray200, + gray300, + gray800, + brand600, + gray900, + gray600, + gray700, + error300, + error25, + error500, + gray500 +} = vars; + +const SIDEBAR_STYLES = { + expanded: '35rem', + collapsed: '5.75rem', + transition: 'all 0.5s ease', +}; + +const EXACT_MATCH_STYLES = { + border: `1px solid ${error300}`, + boxShadow: '0 1px 2px 0 rgba(16, 24, 40, 0.05), 0 0 0 4px rgba(240, 68, 56, 0.24)', + backgroundColor: error25, + borderRadius: '0.5rem' +}; + +const HOVER_INDICATOR_STYLES = { + position: 'absolute', + left: 0, + content: '""', + height: '1.5rem', + borderRadius: '3px', + width: '2px', + background: brand600 +}; + +const LoadingSpinner = () => ( + + + +); + +const ExpandedHeader = ({ onToggle }) => ( + + + Potential matches + + + + + + + +); + +const CollapsedHeader = ({ onToggle }) => ( + + + + + + + +); + +const EmptyState = () => ( + + + + + + + No match found + + + Add a label to your term to visualize potential matches. + + + +); + +const ResultItem = ({ result, searchValue }) => { + const isExactMatch = result.label.toLowerCase() === searchValue?.toLowerCase(); + + const getItemStyles = () => ({ + borderBottom: `1px solid ${gray200}`, + position: 'relative', + ...(isExactMatch && EXACT_MATCH_STYLES), + '&:hover': { + ...(!isExactMatch && { + '&:before': HOVER_INDICATOR_STYLES + }) + } + }); return ( + + + + {result.label} + + {isExactMatch && ( + + Central Nervous System + + + )} + + + {isExactMatch && ( + <> + + + + )} + + + + {result.definition} + + + URI + http://uri.interlex.org/Interlex/uris/ontologies/nervous-system12/ + + + ); +}; + +const ResultsList = ({ results, searchValue, isResultsEmpty }) => ( + + {isResultsEmpty ? ( + + ) : ( + <> + {results?.map((result) => ( + + ))} + + )} + +); + +export default function NewTermSidebar({ + open, + loading, + onToggle, + results, + isResultsEmpty, + searchValue +}) { + if (loading) { + return ; + } + + const sidebarStyles = { + display: 'flex', + flexDirection: 'column', + borderLeft: `1px solid ${gray200}`, + transition: SIDEBAR_STYLES.transition, + p: 3, + maxWidth: open ? SIDEBAR_STYLES.expanded : SIDEBAR_STYLES.collapsed, + overflowY: 'auto', + '::-webkit-scrollbar': { + display: 'none', + }, + scrollbarWidth: 'none', + position: 'relative', + }; + + return ( + {open ? ( - - Potential matches - - - - - - + ) : ( - - - - - - - + )} - {(open && results )&& ( - - {isResultsEmpty ? ( - - - - - - No match found - Add a label to your term to visualize potential matches. - - - ) : ( - <>{results?.map((result) => ( - - {result.label} - - {result.description} - - - ))} - )} - + + {open && results && ( + )} - + ); } @@ -105,4 +286,5 @@ NewTermSidebar.propTypes = { onToggle: PropTypes.func.isRequired, results: PropTypes.array.isRequired, isResultsEmpty: PropTypes.bool.isRequired, -}; + searchValue: PropTypes.string.isRequired +}; \ No newline at end of file diff --git a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx index 8bc3836e..ef76f2f0 100644 --- a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx +++ b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx @@ -1,7 +1,5 @@ -import { useState, useContext, useMemo, useCallback, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; +import { useState } from "react"; import PropTypes from "prop-types"; -import { debounce } from 'lodash'; import { Box, Divider, @@ -18,24 +16,12 @@ import OntologySearch from '../../SingleTermView/OntologySearch'; import FirstStepContent from "./FirstStepContent"; import SecondStepContent from "./SecondStepContent"; import StatusStep from "../../common/StatusStep"; -import { GlobalDataContext } from '../../../contexts/DataContext'; import { getAddTermStatusProps } from '../termStatusProps'; -import { getEndpointsIlx, elasticSearch } from '../../../api/endpoints/index'; import { CheckedIcon, UncheckedIcon } from '../../../Icons'; import { vars } from "../../../theme/variables"; const { gray100, gray200, gray400 } = vars; -const INITIAL_FORM_STATE = { - label: "", - synonyms: [], - superClass: "", - existingIDs: [], - isDefinedBy: "", - description: "", - comment: "" -}; - const HeaderRightSideContent = ({ activeStep, onContinue, @@ -70,7 +56,7 @@ const HeaderRightSideContent = ({ Cancel - ) } -export default SecondStepContent; \ No newline at end of file +export default SecondStepContent; diff --git a/src/components/common/CustomButtonGroup.jsx b/src/components/common/CustomButtonGroup.jsx index b4f3b3a2..4b2da018 100644 --- a/src/components/common/CustomButtonGroup.jsx +++ b/src/components/common/CustomButtonGroup.jsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import ButtonGroup from '@mui/material/ButtonGroup'; import ClickAwayListener from '@mui/material/ClickAwayListener'; @@ -17,7 +18,8 @@ const styles = { borderRadius: "0.5rem", border: `1px solid ${gray200}`, backgroundColor: white, - boxShadow: paperShadow + boxShadow: paperShadow, + width: "100%" }, menu: { padding: "0.25rem 0" @@ -32,7 +34,7 @@ const styles = { } } -const CustomButtonGroup = ({ +const CustomButtonGroup = ({ buttonTitle, buttonIcon = null, options = [], @@ -68,7 +70,7 @@ const CustomButtonGroup = ({ }; return ( - + {buttonTitle} @@ -91,6 +99,7 @@ const CustomButtonGroup = ({ aria-label="split button with menu" aria-haspopup="menu" onClick={handleToggle} + sx={{ pl: "0.75rem !important" }} > {open ? : } @@ -98,7 +107,7 @@ const CustomButtonGroup = ({ {options.length > 0 && ( )} - + ); } From f841057c6c73d45ca6ed9e561f3dc92874275180 Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Fri, 1 Aug 2025 14:13:14 +0200 Subject: [PATCH 03/18] ILEX-99 make general changes of input --- .../EditBulkTerms/EditBulkAttributesForm.jsx | 11 +++-- .../Dashboard/EditBulkTerms/SearchTerms.jsx | 44 +++++++++++-------- .../OverView/AddPredicateDialog.jsx | 31 ++++++++----- .../TermEditor/AddPredicatesStep.jsx | 11 ++++- src/components/TermEditor/ImportFileTab.jsx | 20 +++++++-- .../TermEditor/newTerm/AddNewTermDialog.jsx | 2 +- .../TermEditor/newTerm/FirstStepContent.jsx | 10 ++++- .../TermEditor/newTerm/SecondStepContent.jsx | 29 ++++++++++-- src/components/common/CustomInputBox.jsx | 11 ++++- 9 files changed, 121 insertions(+), 48 deletions(-) diff --git a/src/components/Dashboard/EditBulkTerms/EditBulkAttributesForm.jsx b/src/components/Dashboard/EditBulkTerms/EditBulkAttributesForm.jsx index 3cc81625..46f56ddd 100644 --- a/src/components/Dashboard/EditBulkTerms/EditBulkAttributesForm.jsx +++ b/src/components/Dashboard/EditBulkTerms/EditBulkAttributesForm.jsx @@ -5,7 +5,7 @@ import { Box } from "@mui/material"; import PropTypes from "prop-types"; -import CustomizedInput from "../../common/CustomizedInput"; +import CustomInputBox from "../../common/CustomInputBox"; import AddOutlinedIcon from "@mui/icons-material/AddOutlined"; import CustomSingleSelect from "../../common/CustomSingleSelect"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; @@ -13,7 +13,7 @@ import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import { vars } from "../../../theme/variables"; const { gray800 } = vars; -const EditBulkAttributesForm = ({columns, attributes, setAttributes, initialAttributesValue}) => { +const EditBulkAttributesForm = ({ columns, attributes, setAttributes, initialAttributesValue }) => { const handleAttributesChange = (index, field, value) => { let newAttributes = [...attributes]; newAttributes[index][field] = value; @@ -82,10 +82,15 @@ const EditBulkAttributesForm = ({columns, attributes, setAttributes, initialAttr /> - handleAttributesChange(index, 'value', e.target.value)} + /> */} + handleAttributesChange(index, 'value', e.target.value)} /> diff --git a/src/components/Dashboard/EditBulkTerms/SearchTerms.jsx b/src/components/Dashboard/EditBulkTerms/SearchTerms.jsx index 5b069152..6a0d01f1 100644 --- a/src/components/Dashboard/EditBulkTerms/SearchTerms.jsx +++ b/src/components/Dashboard/EditBulkTerms/SearchTerms.jsx @@ -13,7 +13,7 @@ import { RadioGroup } from "@mui/material"; import DropDownConditions from "./DropDownConditions"; -import CustomizedInput from "../../common/CustomizedInput"; +import CustomInputBox from "../../common/CustomInputBox"; import AddOutlinedIcon from "@mui/icons-material/AddOutlined"; import CustomSingleSelect from "../../common/CustomSingleSelect"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; @@ -40,7 +40,7 @@ const styles = { }, radioLabel: { "& .MuiFormControlLabel-label": { - color: gray700, + color: gray700, fontWeight: 500 } }, @@ -92,14 +92,14 @@ const SearchTerms = ({ searchConditions, setSearchConditions, initialSearchCondi - Do you want to edit a specific ontology? @@ -110,7 +110,7 @@ const SearchTerms = ({ searchConditions, setSearchConditions, initialSearchCondi value={ontologyEditOption} onChange={handleOntologyEditOptionChange} > - } value={Confirmation.Yes} label="Yes" sx={styles.radioLabel}/> + } value={Confirmation.Yes} label="Yes" sx={styles.radioLabel} /> } value={Confirmation.No} label="No" sx={styles.radioLabel} /> @@ -126,12 +126,12 @@ const SearchTerms = ({ searchConditions, setSearchConditions, initialSearchCondi Add filters {searchConditions.map((term, index) => ( - @@ -182,14 +182,20 @@ const SearchTerms = ({ searchConditions, setSearchConditions, initialSearchCondi 1 ? 3 : 4}> - handleTermChange(index, 'value', e.target.value)} + /> */} + handleTermChange(index, 'value', e.target.value)} /> - + {searchConditions.length > 1 && ( 1 ? 1 : 0}> )} + + } + variant="outlined" + options={menuOptions} + /> + { {open ? : } + + Download as { onChange={(e) => handlePredicateChange(index, 'subject', e.target.value)} disabled={true} /> */} - handlePredicateChange(index, 'subject', e.target.value)} - disabled + onChange={(e) => handlePredicateChange(index, 'subject', e.target.value)} + disabled={true} /> diff --git a/src/components/TermEditor/ImportFileTab.jsx b/src/components/TermEditor/ImportFileTab.jsx index 52a0e963..1266de22 100644 --- a/src/components/TermEditor/ImportFileTab.jsx +++ b/src/components/TermEditor/ImportFileTab.jsx @@ -2,7 +2,8 @@ import PropTypes from "prop-types"; import ImportFile from "./ImportFile"; import { CSVIcon } from "../../Icons"; import Checkbox from "../common/CustomCheckbox"; -import CustomInputBox from "../common/CustomInputBox"; +// import CustomizedInput from "../common/CustomizedInput"; +import CustomFormField from "../common/CustomFormField"; import { Box, Stack, Typography, FormControl, Divider } from "@mui/material"; import { vars } from "../../theme/variables"; @@ -45,11 +46,8 @@ const ImportFileTab = ({ files, url, onFilesChange, onChangeUrl }) => { borderBottomLeftRadius: 0 } }} /> */} - - */} + - - - + /> - Download as { const [open, setOpen] = React.useState(false); const anchorRef = React.useRef(null); @@ -71,7 +72,7 @@ const CustomButtonGroup = ({ }; return ( - + Date: Thu, 7 Aug 2025 23:25:09 +0200 Subject: [PATCH 06/18] ILEX-99 refactor inputs --- .../EditBulkTerms/EditBulkAttributesForm.jsx | 6 -- .../Dashboard/EditBulkTerms/SearchTerms.jsx | 7 -- .../AddNewOntologyDialog.jsx | 27 +---- .../SingleOrganization/CreateForkDialog.jsx | 11 --- .../OverView/AddPredicateDialog.jsx | 8 -- .../TermEditor/AddPredicatesStep.jsx | 8 -- src/components/TermEditor/ImportFileTab.jsx | 10 -- src/components/TermEditor/ManualImportTab.jsx | 62 +++--------- src/components/TermEditor/TermForm.jsx | 69 +++---------- src/components/common/CustomFormField.jsx | 14 ++- src/components/common/CustomInputBox.jsx | 87 ---------------- src/components/common/CustomizedInput.jsx | 99 ------------------- 12 files changed, 37 insertions(+), 371 deletions(-) delete mode 100644 src/components/common/CustomInputBox.jsx delete mode 100644 src/components/common/CustomizedInput.jsx diff --git a/src/components/Dashboard/EditBulkTerms/EditBulkAttributesForm.jsx b/src/components/Dashboard/EditBulkTerms/EditBulkAttributesForm.jsx index 2dc22cee..78f65474 100644 --- a/src/components/Dashboard/EditBulkTerms/EditBulkAttributesForm.jsx +++ b/src/components/Dashboard/EditBulkTerms/EditBulkAttributesForm.jsx @@ -5,7 +5,6 @@ import { Box } from "@mui/material"; import PropTypes from "prop-types"; -// import CustomizedInput from "../../common/CustomizedInput"; import CustomFormField from "../../common/CustomFormField"; import AddOutlinedIcon from "@mui/icons-material/AddOutlined"; import CustomSingleSelect from "../../common/CustomSingleSelect"; @@ -83,11 +82,6 @@ const EditBulkAttributesForm = ({ columns, attributes, setAttributes, initialAtt /> - {/* handleAttributesChange(index, 'value', e.target.value)} - /> */} 1 ? 3 : 4}> - {/* handleTermChange(index, 'value', e.target.value)} - /> */} { {tabValue === 0 && ( - + - {/* */} { isRequired placeholder={"Type your Ontology title"} isEndAdornmentVisible + textFontSize="body1" /> - {/* */} diff --git a/src/components/SingleOrganization/CreateForkDialog.jsx b/src/components/SingleOrganization/CreateForkDialog.jsx index 064a5b01..a3174c5c 100644 --- a/src/components/SingleOrganization/CreateForkDialog.jsx +++ b/src/components/SingleOrganization/CreateForkDialog.jsx @@ -2,7 +2,6 @@ import * as React from "react"; import { useContext } from "react"; import { debounce } from 'lodash'; import PropTypes from "prop-types"; -// import CustomInputBox from "../common/CustomInputBox"; import CustomFormField from "../common/CustomFormField"; import CustomSelectBox from "../common/CustomSelectBox"; import { useState, useEffect, useCallback } from "react"; @@ -131,16 +130,6 @@ const CreateForkDialog = ({ open, handleClose, onSubmit }) => { / - {/* */} ( - {/* handlePredicateChange(index, 'subject', e.target.value)} - disabled={true} - /> */} { {predicates.map((predicate, index) => ( - {/* handlePredicateChange(index, 'subject', e.target.value)} - disabled={true} - /> */} { https:// - {/* */} { return ( - {/* */} - - - ) : ( - - )} - -); +}) => { + const [ontologyChecked, setOntologyChecked] = useState(false); + + const handleOntologyChange = (event) => { + setOntologyChecked(event.target.checked); + }; + + return ( + + {activeStep !== 2 ? ( + <> + } + checkedIcon={} + checked={ontologyChecked} + onChange={handleOntologyChange} + /> + } + sx={{ color: gray600 }} + label="Add to ontology" + /> + + + + + + Cancel + + + + ) : ( + + )} + + ) +}; HeaderRightSideContent.propTypes = { activeStep: PropTypes.number.isRequired, From 68aa461bc507b020ff67a9ec1640a707bb1ce88b Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Wed, 20 Aug 2025 10:46:10 +0200 Subject: [PATCH 09/18] ILEX-99 make changes to connecting logic --- src/components/TermEditor/NewTermSidebar.jsx | 19 +- .../TermEditor/newTerm/AddNewTermDialog.jsx | 70 +++++-- .../TermEditor/newTerm/FirstStepContent.jsx | 163 ++++++++++----- src/components/common/CustomFormField.jsx | 185 ++++++++++++------ 4 files changed, 296 insertions(+), 141 deletions(-) diff --git a/src/components/TermEditor/NewTermSidebar.jsx b/src/components/TermEditor/NewTermSidebar.jsx index 3f4f3aea..8e2c4d99 100644 --- a/src/components/TermEditor/NewTermSidebar.jsx +++ b/src/components/TermEditor/NewTermSidebar.jsx @@ -21,11 +21,9 @@ const { brand600, gray900, gray600, - gray700, error300, error25, error500, - gray500 } = vars; const SIDEBAR_STYLES = { @@ -129,7 +127,7 @@ const EmptyState = () => ( ); -const ResultItem = ({ result, searchValue }) => { +const ResultItem = ({ result, searchValue, onResultAction }) => { const isExactMatch = result.label.toLowerCase() === searchValue?.toLowerCase(); const getItemStyles = () => ({ @@ -182,6 +180,7 @@ const ResultItem = ({ result, searchValue }) => { height: "auto", '&:hover': { backgroundColor: "transparent" } }} + onClick={() => onResultAction(result)} > Go to term @@ -201,15 +200,11 @@ const ResultItem = ({ result, searchValue }) => { > {result.definition} - - URI - http://uri.interlex.org/Interlex/uris/ontologies/nervous-system12/ - ); }; -const ResultsList = ({ results, searchValue, isResultsEmpty }) => ( +const ResultsList = ({ results, searchValue, isResultsEmpty, onResultAction }) => ( ( key={result.id || result.ilx} result={result} searchValue={searchValue} + onResultAction={onResultAction} /> ))} @@ -240,7 +236,8 @@ export default function NewTermSidebar({ onToggle, results, isResultsEmpty, - searchValue + searchValue, + onResultAction }) { if (loading) { return ; @@ -274,6 +271,7 @@ export default function NewTermSidebar({ results={results} searchValue={searchValue} isResultsEmpty={isResultsEmpty} + onResultAction={onResultAction} /> )} @@ -286,5 +284,6 @@ NewTermSidebar.propTypes = { onToggle: PropTypes.func.isRequired, results: PropTypes.array.isRequired, isResultsEmpty: PropTypes.bool.isRequired, - searchValue: PropTypes.string.isRequired + searchValue: PropTypes.string.isRequired, + onResultAction: PropTypes.func, }; \ No newline at end of file diff --git a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx index 60db9030..28febfc8 100644 --- a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx +++ b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import { useState, useCallback, useContext } from "react"; +import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import { Box, @@ -16,6 +17,8 @@ import OntologySearch from '../../SingleTermView/OntologySearch'; import FirstStepContent from "./FirstStepContent"; import SecondStepContent from "./SecondStepContent"; import StatusStep from "../../common/StatusStep"; +import { GlobalDataContext } from "../../../contexts/DataContext"; +import { createNewEntity } from "../../../api/endpoints/apiService"; import { getAddTermStatusProps } from '../termStatusProps'; import { CheckedIcon, UncheckedIcon } from '../../../Icons'; import { vars } from "../../../theme/variables"; @@ -26,7 +29,7 @@ const HeaderRightSideContent = ({ activeStep, onContinue, onClose, - isContinueButtonDisabled + isCreateButtonDisabled }) => { const [ontologyChecked, setOntologyChecked] = useState(false); @@ -65,9 +68,8 @@ const HeaderRightSideContent = ({ Cancel @@ -92,29 +94,59 @@ HeaderRightSideContent.propTypes = { activeStep: PropTypes.number.isRequired, onContinue: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, - isContinueButtonDisabled: PropTypes.bool.isRequired + isCreateButtonDisabled: PropTypes.bool.isRequired }; -const AddNewTermDialog = ({ - open, - handleClose, - searchTerm -}) => { +const AddNewTermDialog = ({ open, handleClose, searchTerm }) => { const [activeStep, setActiveStep] = useState(0); const [addTermResponse, setAddTermResponse] = useState(null); - const [termValue, setTermValue] = useState(''); + const [selectedType, setSelectedType] = useState(null); + const [termValue, setTermValue] = useState(""); + const [exactSynonyms, setExactSynonyms] = useState([]); + const [existingIds, setExistingIds] = useState([]); + const [loading, setLoading] = useState(false); + const [hasExactMatch, setHasExactMatch] = useState(false); + const [searchError, setSearchError] = useState(null); + const { user, updateStoredSearchTerm } = useContext(GlobalDataContext); + const navigate = useNavigate(); - const isSearchTermAvailable = Boolean(searchTerm); - const isContinueButtonDisabled = !isSearchTermAvailable; + const isCreateButtonDisabled = hasExactMatch || termValue === ""; const statusProps = getAddTermStatusProps(addTermResponse, termValue); - const handleContinueClick = () => setActiveStep(activeStep + 1); - const handleCancelBtnClick = () => { handleClose(); setActiveStep(0); }; + const createNewTerm = useCallback(async () => { + if (!termValue || !selectedType || hasExactMatch) return; + + setLoading(true); + + const token = localStorage.getItem("token"); + const groupName = user?.groupname || "base"; + const body = { + 'rdf-type': selectedType || 'owl:Class', + label: termValue, + exact: exactSynonyms, + existingIds: existingIds + }; + + try { + const response = await createNewEntity({ + group: groupName, + data: body, + session: token + }); + navigate(`/terms/${response.term.id.split("/").pop()}`); + } catch (error) { + console.error("Creation failed:", error); + setSearchError("Failed to create new term"); + } finally { + setLoading(false); + } + }, [termValue, selectedType, exactSynonyms, existingIds, user, hasExactMatch, navigate]); + return ( } sx={{ '& .MuiDialogContent-root': { padding: 0, overflowY: "hidden" } }} > - {activeStep === 0 && } + {activeStep === 0 && } {activeStep === 1 && } {activeStep === 2 && addTermResponse != null && ( diff --git a/src/components/TermEditor/newTerm/FirstStepContent.jsx b/src/components/TermEditor/newTerm/FirstStepContent.jsx index 52883d30..19b54f51 100644 --- a/src/components/TermEditor/newTerm/FirstStepContent.jsx +++ b/src/components/TermEditor/newTerm/FirstStepContent.jsx @@ -1,17 +1,27 @@ -import { useState, useCallback, useEffect } from "react" -import { debounce } from "lodash" -import { Box, Divider, Stack, Typography, Autocomplete, Chip, TextField } from "@mui/material" -import CloseIcon from "@mui/icons-material/Close" -import CustomSingleSelect from "../../common/CustomSingleSelect" -import CustomFormField from "../../common/CustomFormField" -import NewTermSidebar from "../NewTermSidebar" -import { HelpOutlinedIcon } from "../../../Icons" -import { elasticSearch } from "../../../api/endpoints" -import { vars } from "../../../theme/variables" +import { useState, useCallback, useEffect, useContext } from "react"; +import { debounce } from "lodash"; +import { + Box, + Divider, + Stack, + Typography, + Autocomplete, + Chip, + TextField +} from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import { GlobalDataContext } from "../../../contexts/DataContext"; +import CloseIcon from "@mui/icons-material/Close"; +import CustomSingleSelect from "../../common/CustomSingleSelect"; +import CustomFormField from "../../common/CustomFormField"; +import NewTermSidebar from "../NewTermSidebar"; +import { HelpOutlinedIcon } from "../../../Icons"; +import { elasticSearch } from "../../../api/endpoints"; +import { vars } from "../../../theme/variables"; const { white, gray300, gray400, gray500, gray600 } = vars; -const TYPES = ["Term", "Relationship", "Ontology"] +const TYPES = ["Term", "Relationship", "Ontology"]; const AUTOCOMPLETE_STYLES = { "& .MuiOutlinedInput-root": { @@ -28,14 +38,14 @@ const AUTOCOMPLETE_STYLES = { "& .MuiAutocomplete-tag": { background: "transparent", }, -} +}; const SYNONYM_CHIP_STYLES = { flexDirection: "row !important", "& .MuiChip-deleteIcon": { color: `${gray400} !important`, }, -} +}; const ID_CHIP_STYLES = { flexDirection: "row !important", @@ -43,77 +53,120 @@ const ID_CHIP_STYLES = { "& .MuiChip-deleteIcon": { color: `${gray400} !important`, }, -} +}; -const FirstStepContent = () => { +const FirstStepContent = ({ handleDialogClose }) => { const [termResults, setTermResults] = useState([]); - const [exactSynonyms, setExactSynonyms] = useState([]) - const [existingIds, setExistingIds] = useState([]) - const [openSidebar, setOpenSidebar] = useState(true) - const [selectedType, setSelectedType] = useState(null) - const [termValue, setTermValue] = useState("") + const [exactSynonyms, setExactSynonyms] = useState([]); + const [existingIds, setExistingIds] = useState([]); + const [openSidebar, setOpenSidebar] = useState(true); + const [selectedType, setSelectedType] = useState(null); + const [termValue, setTermValue] = useState(""); const [loading, setLoading] = useState(false); + const [hasExactMatch, setHasExactMatch] = useState(false); + const [searchError, setSearchError] = useState(null); - const [synonymOptions] = useState([]) - const [idOptions] = useState([]) + const [synonymOptions] = useState([]); + const [idOptions] = useState([]); + const { user, updateStoredSearchTerm } = useContext(GlobalDataContext); + const navigate = useNavigate(); - const fetchTerms = useCallback( - debounce((searchTerm, type) => { - if (!searchTerm && !type) { + const searchForMatches = useCallback( + debounce(async (searchTerm, type) => { + if (!searchTerm || !type) { setTermResults([]); + setHasExactMatch(false); return; } setLoading(true); - // it should be elastiSearch(searchTerm, type) - elasticSearch(searchTerm) - .then(data => { - setTermResults(data.results?.results || []); - }) - .catch(error => { - console.error("Search error:", error); - setTermResults([]); - }) - .finally(() => { - setLoading(false); + setSearchError(null); + + try { + const response = await elasticSearch(searchTerm, 10); + const rawResults = response.results.results || []; + + const filteredResults = rawResults.filter(result => { + return result.type === type.toLowerCase() || + (result.type === "term") || + (result.type === "relationship") || + (result.type === "ontology"); + }); + + const exactMatch = filteredResults.find(result => + result.label?.toLowerCase() === searchTerm.toLowerCase() && + result.type === type.toLowerCase() + ); + + setHasExactMatch(!!exactMatch); + + const sortedResults = filteredResults.sort((a, b) => { + const aIsExact = a.label?.toLowerCase() === searchTerm.toLowerCase(); + const bIsExact = b.label?.toLowerCase() === searchTerm.toLowerCase(); + + if (aIsExact && !bIsExact) return -1; + if (!aIsExact && bIsExact) return 1; + return 0; }); + + setTermResults(sortedResults); + } catch (error) { + setSearchError("Failed to search for existing terms"); + setTermResults([]); + setHasExactMatch(false); + } finally { + setLoading(false); + } }, 500), [] ); const handleSynonymChange = (event, newValue) => { - setExactSynonyms(newValue) - } + setExactSynonyms(newValue); + }; const handleExistingIdChange = (event, newValue) => { - setExistingIds(newValue) - } + setExistingIds(newValue); + }; const handleTermValueChange = (event) => { const value = event.target.value; setTermValue(value); - } + }; const handleTypeChange = (newType) => { setSelectedType(newType); - } + }; const handleSidebarToggle = () => setOpenSidebar(!openSidebar); + const navigateToExistingTerm = (searchResult) => { + updateStoredSearchTerm(searchResult?.label); + const groupName = user?.groupname || 'base'; + navigate(`/${groupName}/${searchResult?.ilx}/overview`); + handleDialogClose(); + }; + const renderChips = (values, getTagProps, chipStyles) => { return values.map((option, index) => ( - } sx={chipStyles} {...getTagProps({ index })} /> - )) - } - - const isResultsEmpty = termResults?.length === 0; + } + sx={chipStyles} + {...getTagProps({ index })} + /> + )); + }; useEffect(() => { - fetchTerms(termValue, selectedType); + if (termValue && selectedType) { + searchForMatches(termValue, selectedType); + } return () => { - fetchTerms.cancel(); + searchForMatches.cancel(); }; - }, [termValue, selectedType, fetchTerms]); + }, [termValue, selectedType, searchForMatches]); return ( @@ -160,6 +213,7 @@ const FirstStepContent = () => { value={termValue} onChange={handleTermValueChange} isEndAdornmentVisible + errorMessage={hasExactMatch ? "Your label has an exact match." : null} /> @@ -211,12 +265,15 @@ const FirstStepContent = () => { loading={loading} onToggle={handleSidebarToggle} results={termResults} - isResultsEmpty={isResultsEmpty} + isResultsEmpty={termResults.length === 0} searchValue={termValue} selectedType={selectedType} + onProceedWithCreation={createNewTerm} + error={searchError} + onResultAction={navigateToExistingTerm} /> - ) -} + ); +}; export default FirstStepContent; \ No newline at end of file diff --git a/src/components/common/CustomFormField.jsx b/src/components/common/CustomFormField.jsx index 4116a065..9b4dce47 100644 --- a/src/components/common/CustomFormField.jsx +++ b/src/components/common/CustomFormField.jsx @@ -1,40 +1,61 @@ import PropTypes from "prop-types"; -import { FormControl, Box, OutlinedInput, InputAdornment, Typography } from "@mui/material"; -import { styled } from '@mui/material/styles'; +import { + FormControl, + Box, + InputAdornment, + Typography, + styled +} from "@mui/material"; import InputBase from '@mui/material/InputBase'; import ErrorOutlineOutlinedIcon from '@mui/icons-material/ErrorOutlineOutlined'; import { HelpOutlinedIcon } from "../../Icons"; import { vars } from "../../theme/variables"; -const { gray50, gray300, gray500, brand600, gray600, gray700, gray800 } = vars; +const { + gray50, + gray300, + gray500, + brand600, + gray600, + gray700, + gray800, + error300, + error600, + inputErrorBoxShadow +} = vars; -const BootstrapInput = styled(InputBase)(({ theme }) => ({ +const StyledInput = styled(InputBase)(({ theme, error }) => ({ "& .MuiInputBase-input": { borderRadius: ".5rem", - border: `1px solid ${gray300}`, + border: error ? `1px solid ${error300}` : `1px solid ${gray300}`, fontSize: "1rem", width: "100%", padding: ".5rem .75rem", height: "2.5rem", - color: gray700, + color: error ? error600 : gray700, + "&:focus": { - borderColor: brand600, - boxShadow: "none", + borderColor: error ? error300 : brand600, + boxShadow: error ? inputErrorBoxShadow : "none", borderWidth: "2px", }, + "&::placeholder": { color: gray500, fontSize: "1rem", }, + "&.Mui-disabled": { backgroundColor: gray50, fontWeight: 400, opacity: 1, }, }, + "& .MuiInputBase-root": { position: "relative", }, + "& .MuiInputAdornment-root": { position: "absolute", right: ".75rem", @@ -42,11 +63,76 @@ const BootstrapInput = styled(InputBase)(({ theme }) => ({ transform: "translateY(-50%)", pointerEvents: "auto", }, + "&.MuiInputBase-adornedEnd .MuiInputBase-input": { paddingRight: "2.5rem" } })); +const InputLabelSection = ({ label, isRequired, textFontSize, labelColor }) => ( + + + {label} + + + {isRequired && ( + + Required + + )} + +); + +const getEndAdornment = (endAdornment, errorMessage, isEndAdornmentVisible) => { + if (endAdornment) { + return endAdornment; + } + + if (errorMessage) { + return ( + + + + ); + } + + if (isEndAdornmentVisible) { + return ( + + + + ); + } + + return null; +}; + +const InputMessage = ({ message, isError = false }) => { + if (!message) return null; + + return ( + + {isError ? `${message.charAt(0).toUpperCase() + message.slice(1)}` : message} + + ); +}; + const CustomFormField = ({ label, value, @@ -62,66 +148,47 @@ const CustomFormField = ({ labelColor = gray700, ...otherProps }) => { - const getEndAdornment = () => { - if (endAdornment) { - return endAdornment; - } - - if (errorMessage) { - return ( - - - - ); - } - if (isEndAdornmentVisible) { - return ( - - - - ); - } - return null; - }; - return ( {label && ( - - - {label} - - {isRequired && Required} - + )} - - + - {errorMessage && {`${errorMessage.charAt(0).toUpperCase() + errorMessage.slice(1)}`}} - {helperText && {helperText}} + + + ); @@ -129,16 +196,16 @@ const CustomFormField = ({ CustomFormField.propTypes = { label: PropTypes.string.isRequired, - helperText: PropTypes.string, - placeholder: PropTypes.string, name: PropTypes.string.isRequired, value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + helperText: PropTypes.string, + placeholder: PropTypes.string, endAdornment: PropTypes.node, multiline: PropTypes.bool, rows: PropTypes.number, minRows: PropTypes.number, disabled: PropTypes.bool, - onChange: PropTypes.func.isRequired, errorMessage: PropTypes.string, isRequired: PropTypes.bool, isEndAdornmentVisible: PropTypes.bool, From 3bbcd78d63e4b23e85e4bf257c15a02614a105a6 Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Wed, 20 Aug 2025 11:57:04 +0200 Subject: [PATCH 10/18] ILEX-99 fix logic issue with creating term --- .../TermEditor/newTerm/AddNewTermDialog.jsx | 43 +++++++++++++++--- .../TermEditor/newTerm/FirstStepContent.jsx | 44 +++++-------------- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx index 28febfc8..e0dbed66 100644 --- a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx +++ b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx @@ -8,9 +8,9 @@ import { Stack, Button, FormControlLabel, - Checkbox + Checkbox, + CircularProgress } from "@mui/material"; -import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import CustomButton from '../../common/CustomButton'; import CustomizedDialog from "../../common/CustomizedDialog"; import OntologySearch from '../../SingleTermView/OntologySearch'; @@ -106,8 +106,7 @@ const AddNewTermDialog = ({ open, handleClose, searchTerm }) => { const [existingIds, setExistingIds] = useState([]); const [loading, setLoading] = useState(false); const [hasExactMatch, setHasExactMatch] = useState(false); - const [searchError, setSearchError] = useState(null); - const { user, updateStoredSearchTerm } = useContext(GlobalDataContext); + const { user } = useContext(GlobalDataContext); const navigate = useNavigate(); const isCreateButtonDisabled = hasExactMatch || termValue === ""; @@ -118,6 +117,23 @@ const AddNewTermDialog = ({ open, handleClose, searchTerm }) => { setActiveStep(0); }; + const handleTermValueChange = (event) => { + const value = event.target.value; + setTermValue(value); + }; + + const handleTypeChange = (newType) => { + setSelectedType(newType); + }; + + const handleSynonymChange = (event, newValue) => { + setExactSynonyms(newValue); + }; + + const handleExistingIdChange = (event, newValue) => { + setExistingIds(newValue); + }; + const createNewTerm = useCallback(async () => { if (!termValue || !selectedType || hasExactMatch) return; @@ -141,12 +157,17 @@ const AddNewTermDialog = ({ open, handleClose, searchTerm }) => { navigate(`/terms/${response.term.id.split("/").pop()}`); } catch (error) { console.error("Creation failed:", error); - setSearchError("Failed to create new term"); } finally { setLoading(false); } }, [termValue, selectedType, exactSynonyms, existingIds, user, hasExactMatch, navigate]); + if (loading) { + return + + + } + return ( { } sx={{ '& .MuiDialogContent-root': { padding: 0, overflowY: "hidden" } }} > - {activeStep === 0 && } + {activeStep === 0 && } {activeStep === 1 && } {activeStep === 2 && addTermResponse != null && ( diff --git a/src/components/TermEditor/newTerm/FirstStepContent.jsx b/src/components/TermEditor/newTerm/FirstStepContent.jsx index 19b54f51..8edfe99a 100644 --- a/src/components/TermEditor/newTerm/FirstStepContent.jsx +++ b/src/components/TermEditor/newTerm/FirstStepContent.jsx @@ -55,13 +55,9 @@ const ID_CHIP_STYLES = { }, }; -const FirstStepContent = ({ handleDialogClose }) => { +const FirstStepContent = ({ term, type, existingIds, synonyms, handleTermChange, handleTypeChange, handleExistingIdChange, handleSynonymChange, handleDialogClose }) => { const [termResults, setTermResults] = useState([]); - const [exactSynonyms, setExactSynonyms] = useState([]); - const [existingIds, setExistingIds] = useState([]); const [openSidebar, setOpenSidebar] = useState(true); - const [selectedType, setSelectedType] = useState(null); - const [termValue, setTermValue] = useState(""); const [loading, setLoading] = useState(false); const [hasExactMatch, setHasExactMatch] = useState(false); const [searchError, setSearchError] = useState(null); @@ -121,23 +117,6 @@ const FirstStepContent = ({ handleDialogClose }) => { [] ); - const handleSynonymChange = (event, newValue) => { - setExactSynonyms(newValue); - }; - - const handleExistingIdChange = (event, newValue) => { - setExistingIds(newValue); - }; - - const handleTermValueChange = (event) => { - const value = event.target.value; - setTermValue(value); - }; - - const handleTypeChange = (newType) => { - setSelectedType(newType); - }; - const handleSidebarToggle = () => setOpenSidebar(!openSidebar); const navigateToExistingTerm = (searchResult) => { @@ -160,13 +139,15 @@ const FirstStepContent = ({ handleDialogClose }) => { }; useEffect(() => { - if (termValue && selectedType) { - searchForMatches(termValue, selectedType); + if (term && type) { + searchForMatches(term, type); } return () => { searchForMatches.cancel(); + setHasExactMatch(false); + setTermResults([]) }; - }, [termValue, selectedType, searchForMatches]); + }, [term, type, searchForMatches]); return ( @@ -205,13 +186,13 @@ const FirstStepContent = ({ handleDialogClose }) => { isFormControlFullWidth={true} options={TYPES} placeholder="Select object type" - value={selectedType} + value={type} onChange={handleTypeChange} /> @@ -220,7 +201,7 @@ const FirstStepContent = ({ handleDialogClose }) => { } options={synonymOptions} @@ -266,10 +247,7 @@ const FirstStepContent = ({ handleDialogClose }) => { onToggle={handleSidebarToggle} results={termResults} isResultsEmpty={termResults.length === 0} - searchValue={termValue} - selectedType={selectedType} - onProceedWithCreation={createNewTerm} - error={searchError} + searchValue={term} onResultAction={navigateToExistingTerm} /> From 051c9b7aed78fb097e3711e519d80df97f1db41c Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Wed, 20 Aug 2025 15:12:04 +0200 Subject: [PATCH 11/18] ILEX-99 a small change --- src/components/TermEditor/newTerm/AddNewTermDialog.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx index e0dbed66..9123e913 100644 --- a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx +++ b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx @@ -144,8 +144,7 @@ const AddNewTermDialog = ({ open, handleClose, searchTerm }) => { const body = { 'rdf-type': selectedType || 'owl:Class', label: termValue, - exact: exactSynonyms, - existingIds: existingIds + exact: [], }; try { From d0433201581fdf52f71b4708a25519af754b5609 Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Wed, 20 Aug 2025 15:35:40 +0200 Subject: [PATCH 12/18] ILEX-99 fix linting --- src/components/SingleTermView/index.jsx | 7 +------ src/components/TermEditor/TermDialog.jsx | 2 +- .../TermEditor/newTerm/AddNewTermDialog.jsx | 8 ++++---- .../TermEditor/newTerm/FirstStepContent.jsx | 16 +++++++++++++--- .../TermEditor/newTerm/SecondStepContent.jsx | 2 +- src/components/common/CustomFormField.jsx | 2 +- 6 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/components/SingleTermView/index.jsx b/src/components/SingleTermView/index.jsx index 0b1cf9b5..2ba8c346 100644 --- a/src/components/SingleTermView/index.jsx +++ b/src/components/SingleTermView/index.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useContext, useMemo, useCallback } from "react"; +import { useState, useEffect, useContext, useMemo, useCallback } from "react"; import { Box, Button, @@ -62,7 +62,6 @@ const formatExtensions = { const SingleTermView = () => { const { group, term, tab } = useParams(); const navigate = useNavigate(); - const [open, setOpen] = useState(false); const [dataFormatAnchorEl, setDataFormatAnchorEl] = useState(null); const [isCodeViewVisible, setIsCodeViewVisible] = useState(false); const [toggleButtonValue, setToggleButtonValue] = useState('defaultView'); @@ -281,22 +280,18 @@ const SingleTermView = () => { const handleAddToActiveOntology = () => { console.log('Add term to active ontology'); - setOpen(false); }; const handleCreateFork = () => { console.log('Create fork'); - setOpen(false); }; const handleAddToAnotherOntology = () => { console.log('Add term to another ontology'); - setOpen(false); }; const handleRemoveFromActiveOntology = () => { console.log('Remove from active ontology'); - setOpen(false); }; const menuOptions = [ diff --git a/src/components/TermEditor/TermDialog.jsx b/src/components/TermEditor/TermDialog.jsx index 4fae115a..f2f045ef 100644 --- a/src/components/TermEditor/TermDialog.jsx +++ b/src/components/TermEditor/TermDialog.jsx @@ -9,7 +9,7 @@ import AddNewTermDialogContent from "./AddNewTermDialogContent"; import { Box, Divider, MobileStepper, Stack, Button } from "@mui/material"; import { vars } from "../../theme/variables"; -const { gray100, gray200, gray400, brand700 } = vars; +const { gray100, gray200, gray400 } = vars; const HeaderRightSideContent = ({ activeStep, onContinue, onClose, isContinueButtonDisabled }) => ( diff --git a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx index 9123e913..98ca6571 100644 --- a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx +++ b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx @@ -97,15 +97,15 @@ HeaderRightSideContent.propTypes = { isCreateButtonDisabled: PropTypes.bool.isRequired }; -const AddNewTermDialog = ({ open, handleClose, searchTerm }) => { +const AddNewTermDialog = ({ open, handleClose }) => { const [activeStep, setActiveStep] = useState(0); - const [addTermResponse, setAddTermResponse] = useState(null); + const [addTermResponse] = useState(null); const [selectedType, setSelectedType] = useState(null); const [termValue, setTermValue] = useState(""); const [exactSynonyms, setExactSynonyms] = useState([]); const [existingIds, setExistingIds] = useState([]); const [loading, setLoading] = useState(false); - const [hasExactMatch, setHasExactMatch] = useState(false); + const [hasExactMatch] = useState(false); const { user } = useContext(GlobalDataContext); const navigate = useNavigate(); @@ -159,7 +159,7 @@ const AddNewTermDialog = ({ open, handleClose, searchTerm }) => { } finally { setLoading(false); } - }, [termValue, selectedType, exactSynonyms, existingIds, user, hasExactMatch, navigate]); + }, [termValue, selectedType, user, hasExactMatch, navigate]); if (loading) { return diff --git a/src/components/TermEditor/newTerm/FirstStepContent.jsx b/src/components/TermEditor/newTerm/FirstStepContent.jsx index 8edfe99a..8b739387 100644 --- a/src/components/TermEditor/newTerm/FirstStepContent.jsx +++ b/src/components/TermEditor/newTerm/FirstStepContent.jsx @@ -1,3 +1,4 @@ +import PropTypes from "prop-types"; import { useState, useCallback, useEffect, useContext } from "react"; import { debounce } from "lodash"; import { @@ -60,7 +61,6 @@ const FirstStepContent = ({ term, type, existingIds, synonyms, handleTermChange, const [openSidebar, setOpenSidebar] = useState(true); const [loading, setLoading] = useState(false); const [hasExactMatch, setHasExactMatch] = useState(false); - const [searchError, setSearchError] = useState(null); const [synonymOptions] = useState([]); const [idOptions] = useState([]); @@ -76,7 +76,6 @@ const FirstStepContent = ({ term, type, existingIds, synonyms, handleTermChange, } setLoading(true); - setSearchError(null); try { const response = await elasticSearch(searchTerm, 10); @@ -107,7 +106,6 @@ const FirstStepContent = ({ term, type, existingIds, synonyms, handleTermChange, setTermResults(sortedResults); } catch (error) { - setSearchError("Failed to search for existing terms"); setTermResults([]); setHasExactMatch(false); } finally { @@ -254,4 +252,16 @@ const FirstStepContent = ({ term, type, existingIds, synonyms, handleTermChange, ); }; +FirstStepContent.propTypes = { + term: PropTypes.string, + type: PropTypes.string, + existingIds: PropTypes.array, + synonyms: PropTypes.array, + handleTermChange: PropTypes.func, + handleTypeChange: PropTypes.func, + handleExistingIdChange: PropTypes.func, + handleSynonymChange: PropTypes.func, + handleDialogClose: PropTypes.func, +}; + export default FirstStepContent; \ No newline at end of file diff --git a/src/components/TermEditor/newTerm/SecondStepContent.jsx b/src/components/TermEditor/newTerm/SecondStepContent.jsx index 512b7f5f..ffb47700 100644 --- a/src/components/TermEditor/newTerm/SecondStepContent.jsx +++ b/src/components/TermEditor/newTerm/SecondStepContent.jsx @@ -7,7 +7,7 @@ import PredicateGroupInput from "../../SingleTermView/OverView/PredicateGroupInp import { HelpOutlinedIcon } from "../../../Icons" import { vars } from "../../../theme/variables" -const { white, gray300, gray500, gray600, gray800, gray700 } = vars +const { white, gray300, gray500, gray600, gray800 } = vars const URI_PREFIX = "http://uri.interlex.org/Interlex/uris/" diff --git a/src/components/common/CustomFormField.jsx b/src/components/common/CustomFormField.jsx index 9b4dce47..8aecd210 100644 --- a/src/components/common/CustomFormField.jsx +++ b/src/components/common/CustomFormField.jsx @@ -24,7 +24,7 @@ const { inputErrorBoxShadow } = vars; -const StyledInput = styled(InputBase)(({ theme, error }) => ({ +const StyledInput = styled(InputBase)(({ error }) => ({ "& .MuiInputBase-input": { borderRadius: ".5rem", border: error ? `1px solid ${error300}` : `1px solid ${gray300}`, From 2e79418977a3742072d46252561bb6e0b7e37b63 Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Wed, 20 Aug 2025 16:12:44 +0200 Subject: [PATCH 13/18] ILEX-99 fix linting --- src/components/TermEditor/NewTermSidebar.jsx | 70 +++++++++-------- .../TermEditor/newTerm/FirstStepContent.jsx | 77 +++++++++---------- .../TermEditor/newTerm/SecondStepContent.jsx | 2 +- src/components/common/CustomButtonGroup.jsx | 17 ++++ src/components/common/CustomFormField.jsx | 60 +++++++-------- 5 files changed, 123 insertions(+), 103 deletions(-) diff --git a/src/components/TermEditor/NewTermSidebar.jsx b/src/components/TermEditor/NewTermSidebar.jsx index 8e2c4d99..2e9088bb 100644 --- a/src/components/TermEditor/NewTermSidebar.jsx +++ b/src/components/TermEditor/NewTermSidebar.jsx @@ -204,32 +204,6 @@ const ResultItem = ({ result, searchValue, onResultAction }) => { ); }; -const ResultsList = ({ results, searchValue, isResultsEmpty, onResultAction }) => ( - - {isResultsEmpty ? ( - - ) : ( - <> - {results?.map((result) => ( - - ))} - - )} - -); - export default function NewTermSidebar({ open, loading, @@ -267,17 +241,49 @@ export default function NewTermSidebar({ )} {open && results && ( - + + {isResultsEmpty ? ( + + ) : ( + <> + {results?.map((result) => ( + + ))} + + )} + )} ); } + +ExpandedHeader.propTypes = { + onToggle: PropTypes.func.isRequired, +}; + +CollapsedHeader.propTypes = { + onToggle: PropTypes.func.isRequired, +}; + +ResultItem.propTypes = { + result: PropTypes.object.isRequired, + searchValue: PropTypes.string, + onResultAction: PropTypes.func, +}; + NewTermSidebar.propTypes = { open: PropTypes.bool.isRequired, loading: PropTypes.bool.isRequired, diff --git a/src/components/TermEditor/newTerm/FirstStepContent.jsx b/src/components/TermEditor/newTerm/FirstStepContent.jsx index 8b739387..274dd151 100644 --- a/src/components/TermEditor/newTerm/FirstStepContent.jsx +++ b/src/components/TermEditor/newTerm/FirstStepContent.jsx @@ -1,5 +1,5 @@ import PropTypes from "prop-types"; -import { useState, useCallback, useEffect, useContext } from "react"; +import { useState, useEffect, useContext } from "react"; import { debounce } from "lodash"; import { Box, @@ -67,53 +67,50 @@ const FirstStepContent = ({ term, type, existingIds, synonyms, handleTermChange, const { user, updateStoredSearchTerm } = useContext(GlobalDataContext); const navigate = useNavigate(); - const searchForMatches = useCallback( - debounce(async (searchTerm, type) => { - if (!searchTerm || !type) { - setTermResults([]); - setHasExactMatch(false); - return; - } + const searchForMatches = debounce(async (searchTerm, type) => { + if (!searchTerm || !type) { + setTermResults([]); + setHasExactMatch(false); + return; + } - setLoading(true); + setLoading(true); - try { - const response = await elasticSearch(searchTerm, 10); - const rawResults = response.results.results || []; + try { + const response = await elasticSearch(searchTerm, 10); + const rawResults = response.results.results || []; - const filteredResults = rawResults.filter(result => { - return result.type === type.toLowerCase() || - (result.type === "term") || - (result.type === "relationship") || - (result.type === "ontology"); - }); + const filteredResults = rawResults.filter(result => { + return result.type === type.toLowerCase() || + (result.type === "term") || + (result.type === "relationship") || + (result.type === "ontology"); + }); - const exactMatch = filteredResults.find(result => - result.label?.toLowerCase() === searchTerm.toLowerCase() && - result.type === type.toLowerCase() - ); + const exactMatch = filteredResults.find(result => + result.label?.toLowerCase() === searchTerm.toLowerCase() && + result.type === type.toLowerCase() + ); - setHasExactMatch(!!exactMatch); + setHasExactMatch(!!exactMatch); - const sortedResults = filteredResults.sort((a, b) => { - const aIsExact = a.label?.toLowerCase() === searchTerm.toLowerCase(); - const bIsExact = b.label?.toLowerCase() === searchTerm.toLowerCase(); + const sortedResults = filteredResults.sort((a, b) => { + const aIsExact = a.label?.toLowerCase() === searchTerm.toLowerCase(); + const bIsExact = b.label?.toLowerCase() === searchTerm.toLowerCase(); - if (aIsExact && !bIsExact) return -1; - if (!aIsExact && bIsExact) return 1; - return 0; - }); + if (aIsExact && !bIsExact) return -1; + if (!aIsExact && bIsExact) return 1; + return 0; + }); - setTermResults(sortedResults); - } catch (error) { - setTermResults([]); - setHasExactMatch(false); - } finally { - setLoading(false); - } - }, 500), - [] - ); + setTermResults(sortedResults); + } catch (error) { + setTermResults([]); + setHasExactMatch(false); + } finally { + setLoading(false); + } + }, 500); const handleSidebarToggle = () => setOpenSidebar(!openSidebar); diff --git a/src/components/TermEditor/newTerm/SecondStepContent.jsx b/src/components/TermEditor/newTerm/SecondStepContent.jsx index ffb47700..273f75b8 100644 --- a/src/components/TermEditor/newTerm/SecondStepContent.jsx +++ b/src/components/TermEditor/newTerm/SecondStepContent.jsx @@ -47,7 +47,7 @@ const SecondStepContent = () => { const [transitiveProperty, setTransitiveProperty] = useState("") const [definition, setDefinition] = useState("") const [comment, setComment] = useState("") - const [searchTerm, setSearchTerm] = useState("Central Nervous System") + const [searchTerm] = useState("Central Nervous System") const [predicates, setPredicates] = useState([ { diff --git a/src/components/common/CustomButtonGroup.jsx b/src/components/common/CustomButtonGroup.jsx index 13d54063..825298c7 100644 --- a/src/components/common/CustomButtonGroup.jsx +++ b/src/components/common/CustomButtonGroup.jsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import PropTypes from 'prop-types'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import ButtonGroup from '@mui/material/ButtonGroup'; @@ -148,4 +149,20 @@ const CustomButtonGroup = ({ ); } +CustomButtonGroup.propTypes = { + variant: PropTypes.oneOf(['contained', 'outlined', 'text']), + buttonTitle: PropTypes.node.isRequired, + buttonIcon: PropTypes.node, + options: PropTypes.array, + onOptionSelect: PropTypes.func, + sx: PropTypes.object, +}; + +CustomButtonGroup.defaultProps = { + variant: 'contained', + buttonIcon: null, + options: [], + sx: {}, +}; + export default CustomButtonGroup; \ No newline at end of file diff --git a/src/components/common/CustomFormField.jsx b/src/components/common/CustomFormField.jsx index 8aecd210..4f6565b1 100644 --- a/src/components/common/CustomFormField.jsx +++ b/src/components/common/CustomFormField.jsx @@ -69,30 +69,6 @@ const StyledInput = styled(InputBase)(({ error }) => ({ } })); -const InputLabelSection = ({ label, isRequired, textFontSize, labelColor }) => ( - - - {label} - - - {isRequired && ( - - Required - - )} - -); - const getEndAdornment = (endAdornment, errorMessage, isEndAdornmentVisible) => { if (endAdornment) { return endAdornment; @@ -152,12 +128,27 @@ const CustomFormField = ({ {label && ( - + + + {label} + + + {isRequired && ( + + Required + + )} + )} Date: Thu, 28 Aug 2025 16:19:29 +0200 Subject: [PATCH 14/18] adding reverse proxy for entity check --- vite.config.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/vite.config.js b/vite.config.js index b3b39372..0b7303be 100644 --- a/vite.config.js +++ b/vite.config.js @@ -38,6 +38,48 @@ export default defineConfig({ }); }, }, + '^/([^/]+)/priv/entity-check': { + target: "https://uri.olympiangods.org", + secure: false, + changeOrigin: true, + headers: { + "Content-Type": "application/json", + }, + configure: (proxy, _options) => { + console.log(_options); + proxy.on('error', (err, _req, _res) => { + console.log('proxy error', err); + console.log('proxy request', _req); + console.log('proxy response', _res); + }); + proxy.on('proxyReq', (proxyReq, req, _res) => { + console.log('Sending Request to the Target:', req.method, req.url); + console.log('Headers sent to backend:', proxyReq.getHeaders()); + console.log('Response:', _res); + console.log('Request:', proxyReq); + }); + proxy.on('proxyRes', (proxyRes, req, res) => { + console.log('Received response', res); + console.log('Received Response from the Target:', proxyRes.statusCode, req.url); + const location = proxyRes.headers['location']; + console.log('Received location', location); + if (proxyRes.statusCode === 303 && location) { + // Prevent browser from seeing the actual Location + delete proxyRes.headers['location']; + // Inject the location into a custom header we can use in Axios + res.setHeader('X-Redirect-Location', location); + } + + // Required for credentialed CORS + const origin = req.headers.origin; + if (origin) { + res.setHeader('Access-Control-Allow-Origin', origin); + } + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Expose-Headers', 'X-Redirect-Location'); + }); + }, + }, '^/([^/]+)/priv/(.*)': { target: "https://uri.olympiangods.org", secure: false, From afdb8f50363fc46523f2b583fa948d2f9e173cff Mon Sep 17 00:00:00 2001 From: ddelpiano Date: Fri, 29 Aug 2025 17:40:30 +0200 Subject: [PATCH 15/18] resolving conflict --- vite.config.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/vite.config.js b/vite.config.js index 3612cb2a..ca7b3561 100644 --- a/vite.config.js +++ b/vite.config.js @@ -38,11 +38,7 @@ export default defineConfig({ }); }, }, -<<<<<<< HEAD - '^/([^/]+)/priv/entity-check': { -======= '^/([^/]+)/priv/entity(.*)': { ->>>>>>> fec7e4ceab50ce6aaecb1c1594d95a888be4f7a4 target: "https://uri.olympiangods.org", secure: false, changeOrigin: true, From ddd91ff7db3146fedee9f8e7a8f3cff2682a6757 Mon Sep 17 00:00:00 2001 From: ddelpiano Date: Mon, 1 Sep 2025 17:23:16 +0200 Subject: [PATCH 16/18] fixing routing --- src/App.jsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 4ab6b92d..8a43ced8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -58,6 +58,7 @@ function MainContent() { const { setUserData, loading } = useContext(GlobalDataContext); const navigate = useNavigate(); + const location = useLocation(); // eslint-disable-next-line no-unused-vars const [existingCookies, setCookie, removeCookie] = useCookies(['session']); @@ -75,7 +76,10 @@ function MainContent() { groupname: userData['groupname'], settings: userData }); - navigate("/"); + // Only redirect to home if user is currently on login/register pages + if (location.pathname === '/login' || location.pathname === '/register') { + navigate("/"); + } } catch (error) { console.error("Error fetching user settings:", error); localStorage.removeItem(API_CONFIG.SESSION_DATA.SETTINGS); @@ -89,7 +93,7 @@ function MainContent() { } })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [location.pathname]); if (loading) { return ( From 6372594534d0c1b2e576cad181141f31092e4985 Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Mon, 1 Sep 2025 18:04:49 +0200 Subject: [PATCH 17/18] ILEX-99 make changes to custom button group behaviour --- src/components/Header/index.jsx | 14 ++------- src/components/SingleTermView/index.jsx | 2 -- src/components/common/CustomButtonGroup.jsx | 34 +++++++-------------- 3 files changed, 13 insertions(+), 37 deletions(-) diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index 222dae41..e99207c2 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -365,21 +365,11 @@ const Header = () => { - } - options={options} - onOptionSelect={(option) => console.log('Selected:', option.label)} - /> + ) : ( - } - options={options} - onOptionSelect={(option) => console.log('Selected:', option.label)} - /> + { )} } variant="outlined" options={menuOptions} sx={{ diff --git a/src/components/common/CustomButtonGroup.jsx b/src/components/common/CustomButtonGroup.jsx index 825298c7..8fa5f80e 100644 --- a/src/components/common/CustomButtonGroup.jsx +++ b/src/components/common/CustomButtonGroup.jsx @@ -35,32 +35,24 @@ const styles = { } } -const CustomButtonGroup = ({ - variant = "contained", - buttonTitle, - buttonIcon = null, - options = [], - onOptionSelect, - sx -}) => { +const CustomButtonGroup = ({ variant = "contained", options = [], sx }) => { const [open, setOpen] = React.useState(false); + const [selectedIndex, setSelectedIndex] = React.useState(0); const anchorRef = React.useRef(null); const handleMainButtonClick = () => { - if (options.length === 0 && onOptionSelect) { - onOptionSelect(null); + setOpen(false); + if (options[selectedIndex]?.action) { + options[selectedIndex].action(); } }; - const handleMenuItemClick = (event, option) => { + const handleMenuItemClick = (event, index) => { + setSelectedIndex(index); setOpen(false); - if (option.action) { - option.action(); - } else if (onOptionSelect) { - onOptionSelect(option); - } }; + const handleToggle = () => { setOpen((prevOpen) => !prevOpen); }; @@ -84,7 +76,7 @@ const CustomButtonGroup = ({ > {options.length > 0 && (