diff --git a/src/App.jsx b/src/App.jsx index fb6082a9..4ab6b92d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -126,21 +126,13 @@ function MainContent() { } /> } /> - - - - } - /> @@ -201,6 +193,14 @@ function MainContent() { } /> + + + + } + /> diff --git a/src/api/endpoints/apiService.ts b/src/api/endpoints/apiService.ts index bba23e99..8475050e 100644 --- a/src/api/endpoints/apiService.ts +++ b/src/api/endpoints/apiService.ts @@ -60,9 +60,9 @@ export const userLogout = (group: string) => { return createGetRequest(endpoint, "application/json")(); }; -export const getSelectedTermLabel = async (searchTerm: string): Promise => { +export const getSelectedTermLabel = async (searchTerm: string, group: string = 'base'): Promise<{ label: string | undefined; actualGroup: string }> => { try { - const response = await createGetRequest(`/base/${searchTerm}.jsonld`)(); + const response = await createGetRequest(`/${group}/${searchTerm}.jsonld`)(); const label = response['@graph']?.[0]?.['rdfs:label']; @@ -77,10 +77,39 @@ export const getSelectedTermLabel = async (searchTerm: string): Promise(`/base/${searchTerm}.jsonld`)(); + const fallbackLabel = fallbackResponse['@graph']?.[0]?.['rdfs:label']; + + const getLabelValue = (label: LabelType): string => { + if (typeof label === 'string') return label; + if (Array.isArray(label)) { + const en = label.find( + l => typeof l === 'string' || (typeof l === 'object' && l?.['@language'] === 'en') + ); + return typeof en === 'string' ? en : en?.['@value'] || ''; + } + return label?.['@value'] || ''; + }; + + return { + label: fallbackLabel ? getLabelValue(fallbackLabel) : undefined, + actualGroup: 'base' + }; + } catch (fallbackErr: any) { + console.error('Fallback request also failed:', fallbackErr.message); + return { label: undefined, actualGroup: group }; + } + } + return { label: undefined, actualGroup: group }; } }; @@ -222,6 +251,16 @@ export const getMatchTerms = async (group: string, term: string, filters = {}) = return termParser(response, term); } catch (err: any) { console.error(err.message); + // If the request fails and we're not already trying 'base', try with 'base' as fallback + if (group !== 'base') { + try { + const fallbackResponse = await createGetRequest(`/base/${term}.${BASE_EXTENSION}`, "application/json")(); + return termParser(fallbackResponse, term); + } catch (fallbackErr: any) { + console.error('Fallback request also failed:', fallbackErr.message); + return undefined; + } + } return undefined; } }; @@ -232,6 +271,16 @@ export const getRawData = async (group: string, termID: string, format: string) return response; } catch (err: any) { console.error(err.message); + // If the request fails and we're not already trying 'base', try with 'base' as fallback + if (group !== 'base') { + try { + const fallbackResponse = await createGetRequest(`/base/${termID}.${format}`, "application/json")(); + return fallbackResponse; + } catch (fallbackErr: any) { + console.error('Fallback request also failed:', fallbackErr.message); + return undefined; + } + } return undefined; } }; diff --git a/src/components/GraphViewer/GraphStructure.jsx b/src/components/GraphViewer/GraphStructure.jsx index bef5f554..f6090c86 100644 --- a/src/components/GraphViewer/GraphStructure.jsx +++ b/src/components/GraphViewer/GraphStructure.jsx @@ -31,11 +31,15 @@ export const getGraphStructure = (pred) => { let getExistingObject = uniqueObjects?.find( c => c.id === child.object ); if ( getExistingObject ) { + // Object already exists, just add it to the predicate's children let getExistingPredicate = data.children?.find( c => c.id === child.predicate ); if ( getExistingPredicate ) { getExistingPredicate.children.push(newChild) } } else { + // New object, add it to uniqueObjects and process normally + uniqueObjects.push(newChild); + let getExistingPredicate = data?.children?.find( c => c.id === child.predicate ); if ( getExistingPredicate ) { getExistingPredicate.children.push(newChild) diff --git a/src/components/Header/Search.jsx b/src/components/Header/Search.jsx index 972fd6dd..d5707325 100644 --- a/src/components/Header/Search.jsx +++ b/src/components/Header/Search.jsx @@ -90,7 +90,12 @@ const Search = () => { const [terms, setTerms] = useState([]); const [organizations, setOrganizations] = useState([]); const [ontologies, setOntologies] = useState([]); - const { storedSearchTerm, updateStoredSearchTerm} = useContext(GlobalDataContext) + const { storedSearchTerm, updateStoredSearchTerm, user } = useContext(GlobalDataContext); + + // Get the group name based on user login status + const getGroupName = () => { + return user?.groupname || 'base'; + }; const handleOpenList = () => setOpenList(true); const handleCloseList = () => setOpenList(false); @@ -100,13 +105,15 @@ const Search = () => { if (!newInputValue) return; handleCloseList(); - navigate(`/view?searchTerm=${newInputValue?.ilx}`); + const groupName = getGroupName(); + navigate(`/${groupName}/${newInputValue?.ilx}/overview`); updateStoredSearchTerm(newInputValue?.label) }; const handleSearchTermClick = () => { handleCloseList(); - navigate(`/search?searchTerm=${searchTerm}`); + const groupName = getGroupName(); + navigate(`/${groupName}/search?searchTerm=${searchTerm}`); }; const handleInputFocus = (event) => { diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index a29c0dc1..e782d8b0 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -141,18 +141,6 @@ const NavMenu = [ } ] -const UserNavMenu = [ - { - label: 'My dashboard', - icon: , - href: '/dashboard' - }, - { - label: 'Log out', - icon: - } -] - const Header = () => { const [anchorEl, setAnchorEl] = React.useState(null); const [anchorElUser, setAnchorElUser] = React.useState(null); @@ -164,6 +152,23 @@ const Header = () => { // eslint-disable-next-line no-unused-vars const [existingCookies, setCookie, removeCookie] = useCookies(['session']); + // Get the group name based on user login status + const getGroupName = () => { + return user?.groupname || 'base'; + }; + + const UserNavMenu = [ + { + label: 'My dashboard', + icon: , + href: `/${getGroupName()}/dashboard` + }, + { + label: 'Log out', + icon: + } + ]; + const handleNewTermDialogClose = () => { setOpenNewTermDialog(false); } diff --git a/src/components/SearchResults/ListView.jsx b/src/components/SearchResults/ListView.jsx index 5f802722..a5958bfa 100644 --- a/src/components/SearchResults/ListView.jsx +++ b/src/components/SearchResults/ListView.jsx @@ -13,9 +13,11 @@ const { gray200, gray500, gray700, brand50, brand200, brand600, brand700, error5 const TitleSection = ({ searchResult }) => { const navigate = useNavigate(); + const { user } = useContext(GlobalDataContext); const handleClick = (e, term) => { - navigate(`/view?searchTerm=${term}`); + const groupName = user?.groupname || 'base'; + navigate(`/${groupName}/${term}/overview`); }; return ( @@ -131,11 +133,12 @@ const InfoSection = ({ searchResult }) => { const ListView = ({ searchResults, loading }) => { const navigate = useNavigate(); - const { updateStoredSearchTerm } = useContext(GlobalDataContext); + const { updateStoredSearchTerm, user } = useContext(GlobalDataContext); const handleClick = (searchResult) => { - updateStoredSearchTerm(searchResult?.label) - navigate(`/view?searchTerm=${searchResult?.ilx}`); + updateStoredSearchTerm(searchResult?.label); + const groupName = user?.groupname || 'base'; + navigate(`/${groupName}/${searchResult?.ilx}/overview`); }; diff --git a/src/components/SingleTermView/OverView/OverView.jsx b/src/components/SingleTermView/OverView/OverView.jsx index 9deac00d..41d4078d 100644 --- a/src/components/SingleTermView/OverView/OverView.jsx +++ b/src/components/SingleTermView/OverView/OverView.jsx @@ -12,7 +12,7 @@ import RawDataViewer from "./RawDataViewer"; import { useCallback, useEffect, useMemo, useState } from "react"; import { getMatchTerms, getRawData } from "../../../api/endpoints/apiService"; -const OverView = ({ searchTerm, isCodeViewVisible, selectedDataFormat }) => { +const OverView = ({ searchTerm, isCodeViewVisible, selectedDataFormat, group = "base" }) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [jsonData, setJsonData] = useState(null); @@ -21,21 +21,21 @@ const OverView = ({ searchTerm, isCodeViewVisible, selectedDataFormat }) => { const fetchTerms = useCallback( debounce((searchTerm) => { if (searchTerm) { - getMatchTerms("base", searchTerm).then(data => { + getMatchTerms(group, searchTerm).then(data => { console.log("data from api call: ", data) setData(data?.results?.[0]); setLoading(false); }); } }, 300), - [] + [group] ); const fetchJSONFile = useCallback(() => { - getRawData("base", searchTerm, 'jsonld').then(rawResponse => { + getRawData(group, searchTerm, 'jsonld').then(rawResponse => { setJsonData(rawResponse); }) - }, [searchTerm]); + }, [searchTerm, group]); useEffect(() => { setLoading(true); @@ -75,7 +75,8 @@ const OverView = ({ searchTerm, isCodeViewVisible, selectedDataFormat }) => { OverView.propTypes = { searchTerm: PropTypes.string, isCodeViewVisible: PropTypes.bool, - selectedDataFormat: PropTypes.string + selectedDataFormat: PropTypes.string, + group: PropTypes.string } export default OverView; diff --git a/src/components/SingleTermView/index.jsx b/src/components/SingleTermView/index.jsx index 51cc3423..7191e7e1 100644 --- a/src/components/SingleTermView/index.jsx +++ b/src/components/SingleTermView/index.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useContext } from "react"; +import { useState, useEffect, useRef, useContext, useMemo, useCallback } from "react"; import { Box, Button, @@ -9,10 +9,12 @@ import { Stack, Typography, Menu, - MenuItem + MenuItem, + CircularProgress } from "@mui/material"; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import ToggleButton from '@mui/material/ToggleButton'; +import { useParams, useNavigate } from "react-router-dom"; import CustomBreadcrumbs from "../common/CustomBreadcrumbs"; import ForkRightIcon from '@mui/icons-material/ForkRight'; import { vars } from "../../theme/variables"; @@ -37,7 +39,6 @@ import { } from "@mui/icons-material"; import Discussion from "./Discussion"; import { CodeIcon } from "../../Icons"; -import { useQuery } from "../../helpers"; import CustomSingleSelect from "../common/CustomSingleSelect"; import HomeOutlinedIcon from "@mui/icons-material/HomeOutlined"; import CreateForkDialog from "./CreateForkDialog"; @@ -58,77 +59,105 @@ const formatExtensions = { }; const SingleTermView = () => { + const { group, term, tab } = useParams(); + const navigate = useNavigate(); const [open, setOpen] = useState(false); const actionRef = useRef(null); const anchorRef = useRef(null); const [dataFormatAnchorEl, setDataFormatAnchorEl] = useState(null); - const [tabValue, setTabValue] = useState(0); const [isCodeViewVisible, setIsCodeViewVisible] = useState(false); const [toggleButtonValue, setToggleButtonValue] = useState('defaultView'); const [selectedDataFormat, setSelectedDataFormat] = useState('JSON-LD'); const [openRequestMergeDialog, setOpenRequestMergeDialog] = useState(false); const [editTermDialogOpen, setEditTermDialogOpen] = useState(false); - const query = useQuery(); - const searchTerm = query.get('searchTerm'); - const openDataFormatMenu = Boolean(dataFormatAnchorEl); const [openForkDialog, setOpenForkDialog] = useState(false); + const [termData, setTermData] = useState(null); + const [isLoadingTerm, setIsLoadingTerm] = useState(false); + const [actualGroup, setActualGroup] = useState(group); // Track the actual group the data comes from + const [isUsingFallback, setIsUsingFallback] = useState(false); // Track if we're using fallback data + + // Remove redundant query logic - use term from URL params directly + const searchTerm = term; + const openDataFormatMenu = Boolean(dataFormatAnchorEl); const { storedSearchTerm, updateStoredSearchTerm } = useContext(GlobalDataContext); - const handleForkDialogClose = () => { + // Tab mapping + const tabMapping = useMemo(() => ({ + 'overview': 0, + 'variants': 1, + 'history': 2, + 'discussions': 3 + }), []); + + const tabNames = useMemo(() => ['overview', 'variants', 'history', 'discussions'], []); + const tabLabels = useMemo(() => ["Overview", "Variants", "Version history", "Discussions"], []); + + // Set initial tab value based on URL + const [tabValue, setTabValue] = useState(() => { + return tabMapping[tab] !== undefined ? tabMapping[tab] : 0; + }); + + // Memoize the displayed term label to prevent unnecessary re-renders + const displayedTermLabel = useMemo(() => { + return termData || storedSearchTerm || searchTerm.toUpperCase().replace("_", ":"); + }, [termData, storedSearchTerm, searchTerm]); + + // Memoize breadcrumb items to prevent unnecessary re-renders + const breadcrumbItems = useMemo(() => [ + { label: '', href: '/', icon: HomeOutlinedIcon }, + { label: 'Term search', href: `/${group}/search?searchTerm=${storedSearchTerm}` }, + { label: group, href: '#' }, + { label: displayedTermLabel }, + ], [group, displayedTermLabel, storedSearchTerm]); + + // Optimize handlers with useCallback + const handleChangeTabs = useCallback((event, newValue) => { + setTabValue(newValue); + const newTab = tabNames[newValue]; + navigate(`/${group}/${term}/${newTab}`, { replace: true }); + }, [navigate, group, term, tabNames]); + + const handleForkDialogClose = useCallback(() => { setOpenForkDialog(false); - } + }, []); - const handleOpenForkDialog = () => { + const handleOpenForkDialog = useCallback(() => { setOpenForkDialog(true); - } - const handleClickDataFormatMenu = (event) => { + }, []); + + const handleClickDataFormatMenu = useCallback((event) => { setDataFormatAnchorEl(event.currentTarget); - }; + }, []); - const handleOpenEditTermDialog = () => { + const handleOpenEditTermDialog = useCallback(() => { setEditTermDialogOpen(true); - }; + }, []); - const handleCloseEditTermDialog = () => { + const handleCloseEditTermDialog = useCallback(() => { setEditTermDialogOpen(false); - } - - const handleCloseDataFormatMenu = () => { - setDataFormatAnchorEl(null); - }; + }, []); - const handleDataFormatMenuItemClick = (value) => { - setSelectedDataFormat(value); + const handleCloseDataFormatMenu = useCallback(() => { setDataFormatAnchorEl(null); + }, []); - downloadFormattedData(value); - }; - - const handleOpenRequestMergeDialog = () => { - setOpenRequestMergeDialog(true) - }; + const handleOpenRequestMergeDialog = useCallback(() => { + setOpenRequestMergeDialog(true); + }, []); - const handleCloseRequestMergeDialog = () => { - setOpenRequestMergeDialog(false) - } + const handleCloseRequestMergeDialog = useCallback(() => { + setOpenRequestMergeDialog(false); + }, []); - const onToggleButtonChange = (event, newValue) => { + const onToggleButtonChange = useCallback((event, newValue) => { if (newValue) { - setToggleButtonValue(newValue) - if (newValue === 'codeView') { - setIsCodeViewVisible(true) - } else { - setIsCodeViewVisible(false) - } + setToggleButtonValue(newValue); + setIsCodeViewVisible(newValue === 'codeView'); } - } - - const handleChangeTabs = (event, newValue) => { - setTabValue(newValue); - }; + }, []); - const downloadFormattedData = (dataFormat) => { - getRawData("base", searchTerm, formatExtensions[dataFormat]).then(rawResponse => { + const downloadFormattedData = useCallback((dataFormat) => { + getRawData(actualGroup, searchTerm, formatExtensions[dataFormat]).then(rawResponse => { const formattedData = JSON.stringify(rawResponse, null, 2); const blob = new Blob([formattedData], { type: 'application/json' }); const url = URL.createObjectURL(blob); @@ -140,31 +169,116 @@ const SingleTermView = () => { }).catch(error => { console.error('Error downloading data:', error); }); - } - - const CodeOrTreeIcon = () => { - return isCodeViewVisible ? : - } + }, [actualGroup, searchTerm]); - const breadcrumbItems = [ - { label: '', href: '/', icon: HomeOutlinedIcon }, - { label: 'Term search', href: `/search?searchTerm=${storedSearchTerm}` }, - { label: 'base', href: '#' }, - { label: searchTerm.toUpperCase().replace("_", ":") }, - ]; + const handleDataFormatMenuItemClick = useCallback((value) => { + setSelectedDataFormat(value); + setDataFormatAnchorEl(null); + downloadFormattedData(value); + }, [downloadFormattedData]); + // Optimize data fetching with caching and prevent duplicate calls useEffect(() => { - const fetchLabel = async () => { - const result = await getSelectedTermLabel(searchTerm); - updateStoredSearchTerm(result); + let isMounted = true; + + const fetchTermData = async () => { + if (!searchTerm || !group) return; + + setIsLoadingTerm(true); + try { + const result = await getSelectedTermLabel(searchTerm, group); + if (isMounted) { + setTermData(result.label); + setActualGroup(result.actualGroup); + setIsUsingFallback(result.actualGroup !== group); + if (result.label) { + updateStoredSearchTerm(result.label); + } + } + } catch (error) { + console.error('Error fetching term data:', error); + } finally { + if (isMounted) { + setIsLoadingTerm(false); + } + } + }; + + fetchTermData(); + + return () => { + isMounted = false; }; + }, [searchTerm, group, updateStoredSearchTerm]); // Added group to dependencies + + // Optimize tab URL synchronization + useEffect(() => { + const newTabValue = tabMapping[tab] !== undefined ? tabMapping[tab] : 0; + + if (newTabValue !== tabValue) { + setTabValue(newTabValue); + } + + // If no tab is specified in URL, redirect to overview + if (!tab && group && term) { + navigate(`/${group}/${term}/overview`, { replace: true }); + } + }, [tab, tabMapping, navigate, group, term, tabValue]); - if (searchTerm) { - fetchLabel(); + const isItFork = actualGroup === 'base' ? false : true; // Use actualGroup instead of group + + // Memoize tab content to prevent unnecessary re-renders + const tabContent = useMemo(() => { + switch (tabValue) { + case 0: + return ; + case 1: + return ; + case 2: + return ; + case 3: + return ; + default: + return ; } - }, [searchTerm, updateStoredSearchTerm]); + }, [tabValue, searchTerm, isCodeViewVisible, selectedDataFormat, actualGroup]); - const isItFork = true; + // Memoize the toggle button group for overview tab + const toggleButtonGroup = useMemo(() => { + if (tabValue !== 0) return null; + + return ( + + {isCodeViewVisible && ( + <> + + + Format to visualize: + + setSelectedDataFormat(v)} + options={dataFormats} + /> + + + + )} + + + + + + {isCodeViewVisible ? : } + + + + ); + }, [tabValue, isCodeViewVisible, selectedDataFormat, toggleButtonValue, onToggleButtonChange]); return ( <> @@ -182,10 +296,27 @@ const SingleTermView = () => { - {storedSearchTerm} + {isLoadingTerm ? ( + + ) : ( + displayedTermLabel + )} - + {isItFork ? : null} + {isUsingFallback && ( + + Note: This term is not available in "{group}" group. Showing data from "base" group instead. + + )} @@ -245,51 +376,16 @@ const SingleTermView = () => { - + - - {tabValue === 0 && ( - - {isCodeViewVisible && (<> - - - Format to visualize: - - setSelectedDataFormat(v)} options={dataFormats} /> - - ) - } - - - - - - - - - - )} + + {toggleButtonGroup} - { - tabValue === 0 && - } - { - tabValue === 1 && - } - { - tabValue === 2 && - } - { - tabValue === 3 && - } + {tabContent} @@ -301,4 +397,4 @@ const SingleTermView = () => { ) } -export default SingleTermView \ No newline at end of file +export default SingleTermView diff --git a/src/components/TermEditor/AddNewTermDialogContent.jsx b/src/components/TermEditor/AddNewTermDialogContent.jsx index 41426fa2..31d7fbc4 100644 --- a/src/components/TermEditor/AddNewTermDialogContent.jsx +++ b/src/components/TermEditor/AddNewTermDialogContent.jsx @@ -48,6 +48,7 @@ const AddNewTermDialogContent = ({ activeStep, areMatchesChecked, onMatchesChang const [loading, setLoading] = useState(true); const navigate = useNavigate(); + const { user } = useContext(GlobalDataContext); const [termResults, setTermResults] = useState([]); const [tabValue, setTabValue] = useState(0); const [openSidebar, setOpenSidebar] = useState(true); @@ -60,7 +61,6 @@ const AddNewTermDialogContent = ({ activeStep, areMatchesChecked, onMatchesChang const [url, setUrl] = useState(''); const [formState, setFormState] = useState(initialFormState); const [newTermId, setNewTermId] = useState(""); - const { user } = useContext(GlobalDataContext); const memoData = useMemo(() => data, [data]); @@ -142,7 +142,8 @@ const AddNewTermDialogContent = ({ activeStep, areMatchesChecked, onMatchesChang } const handleGoToTermClick = () => { - navigate(`/view?searchTerm=${newTermId}`); + const groupName = user?.groupname || 'base'; + navigate(`/${groupName}/${newTermId}/overview`); onClose() }