From a347bf74d500aa7ca5ede0eea00425723526f4de Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Thu, 28 Aug 2025 20:58:07 +0200 Subject: [PATCH 01/11] ILEX-136 add check for potential matches logic --- src/api/endpoints/apiService.ts | 26 ++-- .../TermEditor/newTerm/FirstStepContent.jsx | 142 +++++++++++------- src/config.js | 3 +- 3 files changed, 106 insertions(+), 65 deletions(-) diff --git a/src/api/endpoints/apiService.ts b/src/api/endpoints/apiService.ts index b3a0b17a..800e7aa4 100644 --- a/src/api/endpoints/apiService.ts +++ b/src/api/endpoints/apiService.ts @@ -78,9 +78,9 @@ export const getSelectedTermLabel = async (searchTerm: string, group: string = ' return label?.['@value'] || ''; }; - return { - label: label ? getLabelValue(label) : undefined, - actualGroup: group + return { + label: label ? getLabelValue(label) : undefined, + actualGroup: group }; } catch (err: any) { console.error(err.message); @@ -89,7 +89,7 @@ export const getSelectedTermLabel = async (searchTerm: string, group: string = ' try { const fallbackResponse = await createGetRequest(`/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)) { @@ -101,9 +101,9 @@ export const getSelectedTermLabel = async (searchTerm: string, group: string = ' return label?.['@value'] || ''; }; - return { - label: fallbackLabel ? getLabelValue(fallbackLabel) : undefined, - actualGroup: 'base' + return { + label: fallbackLabel ? getLabelValue(fallbackLabel) : undefined, + actualGroup: 'base' }; } catch (fallbackErr: any) { console.error('Fallback request also failed:', fallbackErr.message); @@ -198,13 +198,13 @@ export const createNewOntology = async ({ const redirectLocation = postResponse.headers.get('x-redirect-location'); if (redirectLocation) { - const olympianRedirectLocation = redirectLocation.replace('http://uri.interlex.org','').replace(/\.html$/, '.jsonld'); + const olympianRedirectLocation = redirectLocation.replace('http://uri.interlex.org', '').replace(/\.html$/, '.jsonld'); const getResponse = await fetch(olympianRedirectLocation, { headers: { Authorization: `Bearer ${token}` } }); const jsonResponse = await getResponse.json(); const newOntologyID = jsonResponse?.["@graph"]?.find((object) => object["@type"] === "owl:Ontology")?.["@id"] || null; - + return { created: true, location: olympianRedirectLocation, @@ -299,7 +299,7 @@ export const getTermDiscussions = async (group: string, variantID: string) => { }; export const getVariant = (group: string, term: string) => { - return createGetRequest(`/${group}/variant/${term}`, "application/json")(); + return createGetRequest(`/${group}/variant/${term}`, "application/json")(); }; export const getTermHierarchies = async ({ @@ -328,3 +328,9 @@ export const getTermHierarchies = async ({ } } }; + +export const checkPotentialMatches = async (group: string, data: any) => { + console.log("data: ", data) + console.log("group: ", group) + return createPostRequest(`/${group}${API_CONFIG.REAL_API.CHECK_ENTITY}`, { "Content-Type": "application/json" })(data); +}; \ No newline at end of file diff --git a/src/components/TermEditor/newTerm/FirstStepContent.jsx b/src/components/TermEditor/newTerm/FirstStepContent.jsx index 274dd151..865d5655 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, useEffect, useContext } from "react"; +import { useState, useEffect, useContext, useMemo, useCallback } from "react"; import { debounce } from "lodash"; import { Box, @@ -17,12 +17,13 @@ import CustomSingleSelect from "../../common/CustomSingleSelect"; import CustomFormField from "../../common/CustomFormField"; import NewTermSidebar from "../NewTermSidebar"; import { HelpOutlinedIcon } from "../../../Icons"; -import { elasticSearch } from "../../../api/endpoints"; +import { checkPotentialMatches } from "../../../api/endpoints/apiService"; import { vars } from "../../../theme/variables"; const { white, gray300, gray400, gray500, gray600 } = vars; -const TYPES = ["Term", "Relationship", "Ontology"]; +const TYPES = ['owl:Class', 'owl:AnnotationProperty', 'owl:ObjectProperty', 'TODO:CDE', 'TODO:FDE', 'TODO:PDE']; +const DEFAULT_TYPE = TYPES[0]; const AUTOCOMPLETE_STYLES = { "& .MuiOutlinedInput-root": { @@ -56,73 +57,106 @@ const ID_CHIP_STYLES = { }, }; -const FirstStepContent = ({ term, type, existingIds, synonyms, handleTermChange, handleTypeChange, handleExistingIdChange, handleSynonymChange, handleDialogClose }) => { +const FirstStepContent = ({ + term, + type, + existingIds, + synonyms, + handleTermChange, + handleTypeChange, + handleExistingIdChange, + handleSynonymChange, + handleDialogClose +}) => { + const safeType = type || DEFAULT_TYPE; + const [termResults, setTermResults] = useState([]); const [openSidebar, setOpenSidebar] = useState(true); const [loading, setLoading] = useState(false); const [hasExactMatch, setHasExactMatch] = useState(false); - const [synonymOptions] = useState([]); - const [idOptions] = useState([]); + const synonymOptions = useMemo(() => [], []); + const idOptions = useMemo(() => [], []); + const { user, updateStoredSearchTerm } = useContext(GlobalDataContext); const navigate = useNavigate(); - const searchForMatches = debounce(async (searchTerm, type) => { - if (!searchTerm || !type) { - setTermResults([]); - setHasExactMatch(false); - return; - } + const searchForMatches = useMemo(() => + debounce(async (searchTerm, searchType) => { + if (!searchTerm || !searchType) { + setTermResults([]); + setHasExactMatch(false); + return; + } - setLoading(true); + setLoading(true); - try { - const response = await elasticSearch(searchTerm, 10); - const rawResults = response.results.results || []; + try { + const validType = TYPES.includes(searchType) ? searchType : DEFAULT_TYPE; + console.log("Using type: ", validType); + // 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); + console.log("user: ", user) - const sortedResults = filteredResults.sort((a, b) => { - const aIsExact = a.label?.toLowerCase() === searchTerm.toLowerCase(); - const bIsExact = b.label?.toLowerCase() === searchTerm.toLowerCase(); + const entityCheckResponse = await checkPotentialMatches(user?.groupname || 'base', { + "label": searchTerm, + "rdf-type": validType, + "exact": [] + }); - if (aIsExact && !bIsExact) return -1; - if (!aIsExact && bIsExact) return 1; - return 0; - }); + console.log("Entity Check Response:", entityCheckResponse); + const filteredResults = entityCheckResponse?.matches || []; + const exactMatch = entityCheckResponse?.exact_match || null; - setTermResults(sortedResults); - } catch (error) { - setTermResults([]); - setHasExactMatch(false); - } finally { - setLoading(false); - } - }, 500); + 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; + }); - const handleSidebarToggle = () => setOpenSidebar(!openSidebar); + setTermResults(sortedResults); + } catch (error) { + console.error("Search error:", error); + setTermResults([]); + setHasExactMatch(false); + } finally { + setLoading(false); + } + }, 500), + [user] + ); + + const handleSidebarToggle = useCallback(() => + setOpenSidebar(prev => !prev) + , []); - const navigateToExistingTerm = (searchResult) => { + const navigateToExistingTerm = useCallback((searchResult) => { updateStoredSearchTerm(searchResult?.label); const groupName = user?.groupname || 'base'; navigate(`/${groupName}/${searchResult?.ilx}/overview`); handleDialogClose(); - }; + }, [updateStoredSearchTerm, user, navigate, handleDialogClose]); - const renderChips = (values, getTagProps, chipStyles) => { - return values.map((option, index) => ( + const renderChips = useCallback((values, getTagProps, chipStyles) => + values.map((option, index) => ( - )); - }; + )) + , []); useEffect(() => { - if (term && type) { - searchForMatches(term, type); + if (term && safeType) { + searchForMatches(term, safeType); } return () => { searchForMatches.cancel(); setHasExactMatch(false); - setTermResults([]) + setTermResults([]); }; - }, [term, type, searchForMatches]); + }, [term, safeType, searchForMatches]); return ( @@ -181,7 +215,7 @@ const FirstStepContent = ({ term, type, existingIds, synonyms, handleTermChange, isFormControlFullWidth={true} options={TYPES} placeholder="Select object type" - value={type} + value={safeType} onChange={handleTypeChange} /> Date: Sun, 31 Aug 2025 21:26:33 +0200 Subject: [PATCH 02/11] ILEX-136 replace elastic search with api endpoint --- src/api/endpoints/apiService.ts | 2 - .../TermEditor/newTerm/AddNewTermDialog.jsx | 13 ++- .../TermEditor/newTerm/FirstStepContent.jsx | 88 ++++++++----------- 3 files changed, 50 insertions(+), 53 deletions(-) diff --git a/src/api/endpoints/apiService.ts b/src/api/endpoints/apiService.ts index 800e7aa4..1f34d07a 100644 --- a/src/api/endpoints/apiService.ts +++ b/src/api/endpoints/apiService.ts @@ -330,7 +330,5 @@ export const getTermHierarchies = async ({ }; export const checkPotentialMatches = async (group: string, data: any) => { - console.log("data: ", data) - console.log("group: ", group) return createPostRequest(`/${group}${API_CONFIG.REAL_API.CHECK_ENTITY}`, { "Content-Type": "application/json" })(data); }; \ No newline at end of file diff --git a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx index 98ca6571..6f9e207a 100644 --- a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx +++ b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx @@ -25,6 +25,9 @@ import { vars } from "../../../theme/variables"; const { gray100, gray200, gray400, gray600 } = vars; +const TYPES = ['owl:Class', 'owl:AnnotationProperty', 'owl:ObjectProperty', 'TODO:CDE', 'TODO:FDE', 'TODO:PDE']; +const DEFAULT_TYPE = TYPES[0]; + const HeaderRightSideContent = ({ activeStep, onContinue, @@ -100,12 +103,12 @@ HeaderRightSideContent.propTypes = { const AddNewTermDialog = ({ open, handleClose }) => { const [activeStep, setActiveStep] = useState(0); const [addTermResponse] = useState(null); - const [selectedType, setSelectedType] = useState(null); + const [selectedType, setSelectedType] = useState(DEFAULT_TYPE); const [termValue, setTermValue] = useState(""); const [exactSynonyms, setExactSynonyms] = useState([]); const [existingIds, setExistingIds] = useState([]); const [loading, setLoading] = useState(false); - const [hasExactMatch] = useState(false); + const [hasExactMatch, setHasExactMatch] = useState(false); const { user } = useContext(GlobalDataContext); const navigate = useNavigate(); @@ -134,6 +137,10 @@ const AddNewTermDialog = ({ open, handleClose }) => { setExistingIds(newValue); }; + const handleExactMatchChange = (value) => { + setHasExactMatch(value) + } + const createNewTerm = useCallback(async () => { if (!termValue || !selectedType || hasExactMatch) return; @@ -185,10 +192,12 @@ const AddNewTermDialog = ({ open, handleClose }) => { {activeStep === 0 && { - const safeType = type || DEFAULT_TYPE; - const [termResults, setTermResults] = useState([]); const [openSidebar, setOpenSidebar] = useState(true); const [loading, setLoading] = useState(false); - const [hasExactMatch, setHasExactMatch] = useState(false); const synonymOptions = useMemo(() => [], []); const idOptions = useMemo(() => [], []); @@ -85,58 +84,47 @@ const FirstStepContent = ({ debounce(async (searchTerm, searchType) => { if (!searchTerm || !searchType) { setTermResults([]); - setHasExactMatch(false); + handleExactMatchChange(false); return; } setLoading(true); - try { - const validType = TYPES.includes(searchType) ? searchType : DEFAULT_TYPE; - console.log("Using type: ", validType); - // 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() - // ); + const validType = TYPES.includes(searchType) ? searchType : DEFAULT_TYPE; - console.log("user: ", user) - - const entityCheckResponse = await checkPotentialMatches(user?.groupname || 'base', { + try { + await checkPotentialMatches(user?.groupname || 'base', { "label": searchTerm, "rdf-type": validType, "exact": [] }); - - console.log("Entity Check Response:", entityCheckResponse); - const filteredResults = entityCheckResponse?.matches || []; - const exactMatch = entityCheckResponse?.exact_match || null; - - 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) { - console.error("Search error:", error); - setTermResults([]); - setHasExactMatch(false); + if (error?.response?.status === 409 && error?.response?.data?.existing) { + const existingTerms = error.response.data.existing; + const results = []; + + for (const [termUri, matches] of Object.entries(existingTerms)) { + const matchesArray = Array.isArray(matches) ? matches : [matches]; + + matchesArray.forEach(match => { + results.push({ + ilx: termUri.split('/').pop(), + label: match.object, + predicateInfo: { + existing: match.predicate_existing, + submitted: match.predicate_submitted + } + }); + }); + } + + handleExactMatchChange(true); + setTermResults(results); + } else { + console.error("Non-conflict error:", error); + setTermResults([]); + handleExactMatchChange(false); + } } finally { setLoading(false); } @@ -168,15 +156,15 @@ const FirstStepContent = ({ , []); useEffect(() => { - if (term && safeType) { - searchForMatches(term, safeType); + if (term && type) { + searchForMatches(term, type); } return () => { searchForMatches.cancel(); - setHasExactMatch(false); + handleExactMatchChange(false); setTermResults([]); }; - }, [term, safeType, searchForMatches]); + }, [term, type, searchForMatches]); return ( @@ -215,7 +203,7 @@ const FirstStepContent = ({ isFormControlFullWidth={true} options={TYPES} placeholder="Select object type" - value={safeType} + value={type} onChange={handleTypeChange} /> Date: Mon, 1 Sep 2025 17:57:42 +0200 Subject: [PATCH 03/11] ILEX-136 provide correct header in api call --- src/api/endpoints/apiService.ts | 5 ++++- src/components/TermEditor/newTerm/AddNewTermDialog.jsx | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/api/endpoints/apiService.ts b/src/api/endpoints/apiService.ts index 1f34d07a..11dc5861 100644 --- a/src/api/endpoints/apiService.ts +++ b/src/api/endpoints/apiService.ts @@ -119,12 +119,15 @@ export const createNewEntity = async ({ group, data, session }: { group: string; const endpoint = `/${group}${API_CONFIG.REAL_API.CREATE_NEW_ENTITY}`; const response = await createPostRequest( endpoint, - { "Content-Type": "application/x-www-form-urlencoded" } + { "Content-Type": "application/json" } )(data); + console.log("response: ", response) // If the response is HTML (a string), extract TMP ID if (typeof response === "string") { + console.log("am i here??") const match = response.match(/TMP:\d{9}/); + console.log("match: ", match) if (match) { return { term: { diff --git a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx index 6f9e207a..1ae39349 100644 --- a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx +++ b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx @@ -150,8 +150,7 @@ const AddNewTermDialog = ({ open, handleClose }) => { const groupName = user?.groupname || "base"; const body = { 'rdf-type': selectedType || 'owl:Class', - label: termValue, - exact: [], + label: termValue }; try { @@ -160,6 +159,7 @@ const AddNewTermDialog = ({ open, handleClose }) => { data: body, session: token }); + console.log("response: ", response) navigate(`/terms/${response.term.id.split("/").pop()}`); } catch (error) { console.error("Creation failed:", error); From 3172caaeb8509a845e53a9132102e4f1f08975ca Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Tue, 2 Sep 2025 10:05:27 +0200 Subject: [PATCH 04/11] ILEX-136 make change --- src/api/endpoints/apiService.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/api/endpoints/apiService.ts b/src/api/endpoints/apiService.ts index a2dd7a1a..fbfb7550 100644 --- a/src/api/endpoints/apiService.ts +++ b/src/api/endpoints/apiService.ts @@ -122,20 +122,31 @@ export const createNewEntity = async ({ group, data, session }: { group: string; endpoint, { "Content-Type": "application/json" } )(data); + console.log("response from apiService: ", response); + + // Get the x-redirect-location header from the response + const redirectLocation = response?.headers?.['x-redirect-location']; + if (redirectLocation) { + return { + term: { + id: redirectLocation + }, + raw: response, + status: 200 + }; + } - console.log("response: ", response) // If the response is HTML (a string), extract TMP ID if (typeof response === "string") { - console.log("am i here??") - const match = response.match(/TMP:\d{9}/); - console.log("match: ", match) + const match = response.match(/tmp_\d{9}/); + console.log("match: ", match); if (match) { return { term: { - id: `${match[0]}`, + id: `${match[0]}` }, raw: response, - status: 200, + status: 200 }; } } From 35e9efdc7ef5d9332bf3c2ece6e4eda6efd60a70 Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Thu, 4 Sep 2025 13:52:58 +0200 Subject: [PATCH 05/11] ILEX-136 change the way how to handle potential matches --- src/api/endpoints/apiService.ts | 128 +++++----- .../TermEditor/newTerm/AddNewTermDialog.jsx | 19 +- .../TermEditor/newTerm/FirstStepContent.jsx | 219 +++++++++--------- .../TermEditor/newTerm/SecondStepContent.jsx | 9 +- src/constants/types.js | 12 + 5 files changed, 212 insertions(+), 175 deletions(-) diff --git a/src/api/endpoints/apiService.ts b/src/api/endpoints/apiService.ts index fbfb7550..8b4dd446 100644 --- a/src/api/endpoints/apiService.ts +++ b/src/api/endpoints/apiService.ts @@ -116,64 +116,86 @@ export const getSelectedTermLabel = async (searchTerm: string, group: string = ' }; export const createNewEntity = async ({ group, data, session }: { group: string; data: any; session: string }) => { - try { - const endpoint = `/${group}${API_CONFIG.REAL_API.CREATE_NEW_ENTITY}`; - const response = await createPostRequest( - endpoint, - { "Content-Type": "application/json" } - )(data); - console.log("response from apiService: ", response); - - // Get the x-redirect-location header from the response - const redirectLocation = response?.headers?.['x-redirect-location']; - if (redirectLocation) { - return { - term: { - id: redirectLocation - }, - raw: response, - status: 200 - }; - } - - // If the response is HTML (a string), extract TMP ID - if (typeof response === "string") { - const match = response.match(/tmp_\d{9}/); - console.log("match: ", match); - if (match) { - return { - term: { - id: `${match[0]}` - }, - raw: response, - status: 200 - }; + const endpoint = `/${group}${API_CONFIG.REAL_API.CREATE_NEW_ENTITY}`; + + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + + xhr.open('POST', endpoint, true); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.withCredentials = true; + + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) { + if (xhr.status === 303) { + const redirectUrl = xhr.getResponseHeader('x-redirect-location') || xhr.getResponseHeader('Location'); + if (redirectUrl) { + const tmpMatch = redirectUrl.match(/tmp_\d{9}/); + console.log("Found tmp ID:", tmpMatch?.[0]); + + if (tmpMatch) { + resolve({ + term: { + id: tmpMatch[0] + } + }); + return; + } + } + } } - } - // Otherwise, return response as-is - return response; - } catch (error) { - if (error?.response.status === 409) { - const match = error?.response?.data?.existing?.[0]; - if (match) { - return { - term: { - id: `${match}`, - }, - raw: error?.response, - status: error?.response?.status, - }; + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 0) { + // Try to find tmp ID in the response text + const tmpMatch = xhr.responseText?.match(/tmp_\d{9}/); + if (tmpMatch) { + resolve({ + term: { + id: tmpMatch[0] + }, + raw: xhr.responseText, + status: 303 + }); + return; + } + } + + // Handle 409 Conflict + if (xhr.status === 409) { + try { + const responseData = JSON.parse(xhr.responseText); + const match = responseData?.existing?.[0]; + if (match) { + resolve({ + term: { + id: match + }, + raw: responseData, + status: xhr.status + }); + return; + } + } catch (e) { + console.error("Error parsing 409 response:", e); + } + } + + resolve({ + raw: xhr.responseText, + status: xhr.status + }); } - } + }; - return { - raw: error?.response, - status: error?.response?.status, + xhr.onerror = function() { + console.error("XHR Error:", xhr.status, xhr.statusText); + reject(new Error('Network request failed')); }; - } -}; + xhr.send(JSON.stringify(data)); + }); +} export const createNewOntology = async ({ groupname, @@ -316,7 +338,7 @@ export const getVariant = (group: string, term: string) => { return createGetRequest(`/${group}/variant/${term}`, "application/json")(); }; -export const getTermPredicates = async ({ +export const getTermPredicates = async ({ groupname, termId, objToSub = true, diff --git a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx index 1ae39349..091ccde6 100644 --- a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx +++ b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx @@ -1,5 +1,4 @@ import { useState, useCallback, useContext } from "react"; -import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import { Box, @@ -22,12 +21,10 @@ import { createNewEntity } from "../../../api/endpoints/apiService"; import { getAddTermStatusProps } from '../termStatusProps'; import { CheckedIcon, UncheckedIcon } from '../../../Icons'; import { vars } from "../../../theme/variables"; +import { DEFAULT_TYPE } from "../../../constants/types"; const { gray100, gray200, gray400, gray600 } = vars; -const TYPES = ['owl:Class', 'owl:AnnotationProperty', 'owl:ObjectProperty', 'TODO:CDE', 'TODO:FDE', 'TODO:PDE']; -const DEFAULT_TYPE = TYPES[0]; - const HeaderRightSideContent = ({ activeStep, onContinue, @@ -102,7 +99,7 @@ HeaderRightSideContent.propTypes = { const AddNewTermDialog = ({ open, handleClose }) => { const [activeStep, setActiveStep] = useState(0); - const [addTermResponse] = useState(null); + const [addTermResponse, setAddTermResponse] = useState(null); const [selectedType, setSelectedType] = useState(DEFAULT_TYPE); const [termValue, setTermValue] = useState(""); const [exactSynonyms, setExactSynonyms] = useState([]); @@ -110,7 +107,6 @@ const AddNewTermDialog = ({ open, handleClose }) => { const [loading, setLoading] = useState(false); const [hasExactMatch, setHasExactMatch] = useState(false); const { user } = useContext(GlobalDataContext); - const navigate = useNavigate(); const isCreateButtonDisabled = hasExactMatch || termValue === ""; const statusProps = getAddTermStatusProps(addTermResponse, termValue); @@ -159,14 +155,17 @@ const AddNewTermDialog = ({ open, handleClose }) => { data: body, session: token }); - console.log("response: ", response) - navigate(`/terms/${response.term.id.split("/").pop()}`); + + if (response.term && response.term.id) { + setActiveStep(1); + setAddTermResponse(response.term.id); + } } catch (error) { console.error("Creation failed:", error); } finally { setLoading(false); } - }, [termValue, selectedType, user, hasExactMatch, navigate]); + }, [termValue, selectedType, user, hasExactMatch]); if (loading) { return @@ -202,7 +201,7 @@ const AddNewTermDialog = ({ open, handleClose }) => { handleExistingIdChange={handleExistingIdChange} handleDialogClose={handleClose} />} - {activeStep === 1 && } + {activeStep === 1 && } {activeStep === 2 && addTermResponse != null && ( )} diff --git a/src/components/TermEditor/newTerm/FirstStepContent.jsx b/src/components/TermEditor/newTerm/FirstStepContent.jsx index ea0a1909..c159796c 100644 --- a/src/components/TermEditor/newTerm/FirstStepContent.jsx +++ b/src/components/TermEditor/newTerm/FirstStepContent.jsx @@ -18,68 +18,60 @@ import CustomFormField from "../../common/CustomFormField"; import NewTermSidebar from "../NewTermSidebar"; import { HelpOutlinedIcon } from "../../../Icons"; import { checkPotentialMatches } from "../../../api/endpoints/apiService"; +import { elasticSearch } from "../../../api/endpoints"; import { vars } from "../../../theme/variables"; +import { TYPES, DEFAULT_TYPE } from "../../../constants/types"; const { white, gray300, gray400, gray500, gray600 } = vars; -const TYPES = ['owl:Class', 'owl:AnnotationProperty', 'owl:ObjectProperty', 'TODO:CDE', 'TODO:FDE', 'TODO:PDE']; -const DEFAULT_TYPE = TYPES[0]; - -const AUTOCOMPLETE_STYLES = { - "& .MuiOutlinedInput-root": { - background: white, - borderColor: gray300, - }, - "& .MuiOutlinedInput-root .MuiAutocomplete-input": { - color: gray500, - fontWeight: 400, - }, - "& .MuiAutocomplete-popupIndicator": { - transform: "none !important", - }, - "& .MuiAutocomplete-tag": { - background: "transparent", +const styles = { + autocomplete: { + "& .MuiOutlinedInput-root": { + background: white, + borderColor: gray300, + }, + "& .MuiOutlinedInput-root .MuiAutocomplete-input": { + color: gray500, + fontWeight: 400, + }, + "& .MuiAutocomplete-popupIndicator": { + transform: "none !important", + }, + "& .MuiAutocomplete-tag": { + background: "transparent", + }, }, -}; - -const SYNONYM_CHIP_STYLES = { - flexDirection: "row !important", - "& .MuiChip-deleteIcon": { - color: `${gray400} !important`, - }, -}; - -const ID_CHIP_STYLES = { - flexDirection: "row !important", - borderRadius: "1rem !important", - "& .MuiChip-deleteIcon": { - color: `${gray400} !important`, + chip: { + synonym: { + flexDirection: "row !important", + "& .MuiChip-deleteIcon": { + color: `${gray400} !important`, + }, + }, + id: { + flexDirection: "row !important", + borderRadius: "1rem !important", + "& .MuiChip-deleteIcon": { + color: `${gray400} !important`, + }, + } }, + contentBox: { + px: "3.25rem", + pt: "1.75rem", + pb: "2.5rem", + flex: 1, + overflowY: "auto", + display: "flex", + flexDirection: "column", + gap: "2.75rem", + } }; -const FirstStepContent = ({ - term, - type, - hasExactMatch, - existingIds, - synonyms, - handleTermChange, - handleTypeChange, - handleExactMatchChange, - handleExistingIdChange, - handleSynonymChange, - handleDialogClose -}) => { +const useTermSearch = (user, handleExactMatchChange) => { const [termResults, setTermResults] = useState([]); - const [openSidebar, setOpenSidebar] = useState(true); const [loading, setLoading] = useState(false); - const synonymOptions = useMemo(() => [], []); - const idOptions = useMemo(() => [], []); - - const { user, updateStoredSearchTerm } = useContext(GlobalDataContext); - const navigate = useNavigate(); - const searchForMatches = useMemo(() => debounce(async (searchTerm, searchType) => { if (!searchTerm || !searchType) { @@ -91,37 +83,25 @@ const FirstStepContent = ({ setLoading(true); const validType = TYPES.includes(searchType) ? searchType : DEFAULT_TYPE; + const userGroup = user?.groupname || 'base'; try { - await checkPotentialMatches(user?.groupname || 'base', { - "label": searchTerm, - "rdf-type": validType, - "exact": [] + await checkPotentialMatches(userGroup, { + label: searchTerm, + 'rdf-type': validType, + exact: [] }); + + // No exact matches found - show similar items from elastic search + const { results } = await elasticSearch(searchTerm); + setTermResults(results?.results || []); + handleExactMatchChange(false); } catch (error) { if (error?.response?.status === 409 && error?.response?.data?.existing) { - const existingTerms = error.response.data.existing; - const results = []; - - for (const [termUri, matches] of Object.entries(existingTerms)) { - const matchesArray = Array.isArray(matches) ? matches : [matches]; - - matchesArray.forEach(match => { - results.push({ - ilx: termUri.split('/').pop(), - label: match.object, - predicateInfo: { - existing: match.predicate_existing, - submitted: match.predicate_submitted - } - }); - }); - } - handleExactMatchChange(true); - setTermResults(results); + setTermResults([]); } else { - console.error("Non-conflict error:", error); + console.error("Term search error:", error); setTermResults([]); handleExactMatchChange(false); } @@ -129,17 +109,43 @@ const FirstStepContent = ({ setLoading(false); } }, 500), - [user] + [user, handleExactMatchChange] ); + return { termResults, loading, searchForMatches }; +}; + +const FirstStepContent = ({ + term, + type, + hasExactMatch, + existingIds, + synonyms, + handleTermChange, + handleTypeChange, + handleExactMatchChange, + handleExistingIdChange, + handleSynonymChange, + handleDialogClose +}) => { + const [openSidebar, setOpenSidebar] = useState(true); + const { user, updateStoredSearchTerm } = useContext(GlobalDataContext); + const navigate = useNavigate(); + + const { termResults, loading, searchForMatches } = useTermSearch(user, handleExactMatchChange); + + const synonymOptions = useMemo(() => [], []); + const idOptions = useMemo(() => [], []); + const handleSidebarToggle = useCallback(() => - setOpenSidebar(prev => !prev) - , []); + setOpenSidebar(prev => !prev), []); const navigateToExistingTerm = useCallback((searchResult) => { - updateStoredSearchTerm(searchResult?.label); + if (!searchResult?.label || !searchResult?.ilx) return; + + updateStoredSearchTerm(searchResult.label); const groupName = user?.groupname || 'base'; - navigate(`/${groupName}/${searchResult?.ilx}/overview`); + navigate(`/${groupName}/${searchResult.ilx}/overview`); handleDialogClose(); }, [updateStoredSearchTerm, user, navigate, handleDialogClose]); @@ -152,8 +158,7 @@ const FirstStepContent = ({ sx={chipStyles} {...getTagProps({ index })} /> - )) - , []); + )), []); useEffect(() => { if (term && type) { @@ -162,24 +167,12 @@ const FirstStepContent = ({ return () => { searchForMatches.cancel(); handleExactMatchChange(false); - setTermResults([]); }; - }, [term, type, searchForMatches]); + }, [term, type, searchForMatches, handleExactMatchChange]); return ( - + How would you like to proceed? @@ -223,10 +216,10 @@ const FirstStepContent = ({ popupIcon={} options={synonymOptions} freeSolo - renderTags={(values, getTagProps) => renderChips(values, getTagProps, SYNONYM_CHIP_STYLES)} + renderTags={(values, getTagProps) => renderChips(values, getTagProps, styles.chip.synonym)} fullWidth renderInput={(params) => } - sx={AUTOCOMPLETE_STYLES} + sx={styles.autocomplete} /> @@ -250,10 +243,10 @@ const FirstStepContent = ({ popupIcon={} options={idOptions} freeSolo - renderTags={(values, getTagProps) => renderChips(values, getTagProps, ID_CHIP_STYLES)} + renderTags={(values, getTagProps) => renderChips(values, getTagProps, styles.chip.id)} fullWidth renderInput={(params) => } - sx={AUTOCOMPLETE_STYLES} + sx={styles.autocomplete} /> @@ -273,16 +266,24 @@ const FirstStepContent = ({ FirstStepContent.propTypes = { term: PropTypes.string, - type: PropTypes.string, + type: PropTypes.oneOf(TYPES), hasExactMatch: PropTypes.bool, - existingIds: PropTypes.array, - synonyms: PropTypes.array, - handleTermChange: PropTypes.func, - handleTypeChange: PropTypes.func, - handleExactMatchChange: PropTypes.func, - handleExistingIdChange: PropTypes.func, - handleSynonymChange: PropTypes.func, - handleDialogClose: PropTypes.func, + existingIds: PropTypes.arrayOf(PropTypes.string), + synonyms: PropTypes.arrayOf(PropTypes.string), + handleTermChange: PropTypes.func.isRequired, + handleTypeChange: PropTypes.func.isRequired, + handleExactMatchChange: PropTypes.func.isRequired, + handleExistingIdChange: PropTypes.func.isRequired, + handleSynonymChange: PropTypes.func.isRequired, + handleDialogClose: PropTypes.func.isRequired, +}; + +FirstStepContent.defaultProps = { + term: '', + type: DEFAULT_TYPE, + hasExactMatch: false, + existingIds: [], + synonyms: [], }; 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 273f75b8..e8df731d 100644 --- a/src/components/TermEditor/newTerm/SecondStepContent.jsx +++ b/src/components/TermEditor/newTerm/SecondStepContent.jsx @@ -1,5 +1,6 @@ import React from "react" import { useState } from "react" +import PropTypes from "prop-types"; import { Box, Grid, Typography, FormControl, Autocomplete, Chip, TextField, Divider, Button } from "@mui/material" import AddIcon from "@mui/icons-material/Add" import CustomFormField from "../../common/CustomFormField" @@ -40,15 +41,13 @@ const URI_PREFIX_BOX_STYLES = { padding: "0.5rem 0.75rem", } -const SecondStepContent = () => { +const SecondStepContent = ({ searchTerm }) => { const [superclass, setSuperclass] = useState("") const [subclassOf, setSubclassOf] = useState("") const [definitionUrls, setDefinitionUrls] = useState([]) const [transitiveProperty, setTransitiveProperty] = useState("") const [definition, setDefinition] = useState("") const [comment, setComment] = useState("") - const [searchTerm] = useState("Central Nervous System") - const [predicates, setPredicates] = useState([ { subject: "", @@ -264,4 +263,8 @@ const SecondStepContent = () => { ) } +SecondStepContent.propTypes = { + searchTerm: PropTypes.string +}; + export default SecondStepContent; diff --git a/src/constants/types.js b/src/constants/types.js index 6dab2fd7..a6108cb9 100644 --- a/src/constants/types.js +++ b/src/constants/types.js @@ -3,3 +3,15 @@ export const SEARCH_TYPES = { ORGANIZATION: "organization", ONTOLOGY: "ontology", }; + +export const TERM_TYPES = { + CLASS: 'owl:Class', + ANNOTATION_PROPERTY: 'owl:AnnotationProperty', + OBJECT_PROPERTY: 'owl:ObjectProperty', + CDE: 'TODO:CDE', + FDE: 'TODO:FDE', + PDE: 'TODO:PDE' +}; + +export const TYPES = Object.values(TERM_TYPES); +export const DEFAULT_TYPE = TERM_TYPES.CLASS; From 5dc29dbbfa57f7f661f67897cdcd65f56337e3bb Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Fri, 5 Sep 2025 17:22:25 +0200 Subject: [PATCH 06/11] ILEX-136 add skeleton loader to sidebar and fix connecting logic --- src/components/TermEditor/NewTermSidebar.jsx | 49 ++++++++----- .../TermEditor/newTerm/FirstStepContent.jsx | 68 +++++++++++-------- 2 files changed, 70 insertions(+), 47 deletions(-) diff --git a/src/components/TermEditor/NewTermSidebar.jsx b/src/components/TermEditor/NewTermSidebar.jsx index 2e9088bb..37336c54 100644 --- a/src/components/TermEditor/NewTermSidebar.jsx +++ b/src/components/TermEditor/NewTermSidebar.jsx @@ -8,9 +8,9 @@ import { IconButton, Tooltip, Stack, - CircularProgress, Chip, - Button + Button, + Skeleton } from '@mui/material'; import { vars } from '../../theme/variables'; @@ -49,17 +49,6 @@ const HOVER_INDICATOR_STYLES = { background: brand600 }; -const LoadingSpinner = () => ( - - - -); - const ExpandedHeader = ({ onToggle }) => ( ( ); +const ResultItemSkeleton = () => ( + + + + + + + + + + + + + +); + const ResultItem = ({ result, searchValue, onResultAction }) => { const isExactMatch = result.label.toLowerCase() === searchValue?.toLowerCase(); @@ -213,9 +226,6 @@ export default function NewTermSidebar({ searchValue, onResultAction }) { - if (loading) { - return ; - } const sidebarStyles = { display: 'flex', @@ -223,6 +233,7 @@ export default function NewTermSidebar({ borderLeft: `1px solid ${gray200}`, transition: SIDEBAR_STYLES.transition, p: 3, + minWidth: open ? SIDEBAR_STYLES.expanded : SIDEBAR_STYLES.collapsed, maxWidth: open ? SIDEBAR_STYLES.expanded : SIDEBAR_STYLES.collapsed, overflowY: 'auto', '::-webkit-scrollbar': { @@ -249,7 +260,11 @@ export default function NewTermSidebar({ alignItems="center" gap={1} > - {isResultsEmpty ? ( + {loading ? ( + Array.from(new Array(10)).map((_, index) => ( + + )) + ) : isResultsEmpty ? ( ) : ( <> diff --git a/src/components/TermEditor/newTerm/FirstStepContent.jsx b/src/components/TermEditor/newTerm/FirstStepContent.jsx index c159796c..6c8640ef 100644 --- a/src/components/TermEditor/newTerm/FirstStepContent.jsx +++ b/src/components/TermEditor/newTerm/FirstStepContent.jsx @@ -22,16 +22,17 @@ import { elasticSearch } from "../../../api/endpoints"; import { vars } from "../../../theme/variables"; import { TYPES, DEFAULT_TYPE } from "../../../constants/types"; -const { white, gray300, gray400, gray500, gray600 } = vars; +const { white, gray300, gray400, gray600, gray700 } = vars; const styles = { autocomplete: { "& .MuiOutlinedInput-root": { background: white, borderColor: gray300, + padding: "0.5rem 0.75rem !important" }, "& .MuiOutlinedInput-root .MuiAutocomplete-input": { - color: gray500, + color: gray700, fontWeight: 400, }, "& .MuiAutocomplete-popupIndicator": { @@ -73,7 +74,7 @@ const useTermSearch = (user, handleExactMatchChange) => { const [loading, setLoading] = useState(false); const searchForMatches = useMemo(() => - debounce(async (searchTerm, searchType) => { + debounce(async (searchTerm, searchType, synonyms = []) => { if (!searchTerm || !searchType) { setTermResults([]); handleExactMatchChange(false); @@ -82,29 +83,33 @@ const useTermSearch = (user, handleExactMatchChange) => { setLoading(true); - const validType = TYPES.includes(searchType) ? searchType : DEFAULT_TYPE; - const userGroup = user?.groupname || 'base'; - try { - await checkPotentialMatches(userGroup, { - label: searchTerm, - 'rdf-type': validType, - exact: [] - }); + try { + await checkPotentialMatches(user?.groupname || 'base', { + label: searchTerm, + 'rdf-type': searchType, + exact: synonyms + }); + handleExactMatchChange(false); + } catch (error) { + if (error?.response?.status === 409 && error?.response?.data?.existing) { + handleExactMatchChange(true); + } else { + handleExactMatchChange(false); + } + } - // No exact matches found - show similar items from elastic search const { results } = await elasticSearch(searchTerm); - setTermResults(results?.results || []); - handleExactMatchChange(false); + const similarResults = (results?.results || []).map(result => ({ + ...result, + isExactMatch: false + })); + + setTermResults(similarResults); } catch (error) { - if (error?.response?.status === 409 && error?.response?.data?.existing) { - handleExactMatchChange(true); - setTermResults([]); - } else { - console.error("Term search error:", error); - setTermResults([]); - handleExactMatchChange(false); - } + console.error("Term search error:", error); + setTermResults([]); + handleExactMatchChange(false); } finally { setLoading(false); } @@ -112,7 +117,7 @@ const useTermSearch = (user, handleExactMatchChange) => { [user, handleExactMatchChange] ); - return { termResults, loading, searchForMatches }; + return { termResults, loading, searchForMatches, setTermResults }; }; const FirstStepContent = ({ @@ -132,7 +137,7 @@ const FirstStepContent = ({ const { user, updateStoredSearchTerm } = useContext(GlobalDataContext); const navigate = useNavigate(); - const { termResults, loading, searchForMatches } = useTermSearch(user, handleExactMatchChange); + const { termResults, loading, searchForMatches, setTermResults } = useTermSearch(user, handleExactMatchChange); const synonymOptions = useMemo(() => [], []); const idOptions = useMemo(() => [], []); @@ -161,14 +166,17 @@ const FirstStepContent = ({ )), []); useEffect(() => { + if (!term) { + setTermResults([]); + handleExactMatchChange(false); + return; + } if (term && type) { - searchForMatches(term, type); + searchForMatches(term, type, synonyms); } - return () => { - searchForMatches.cancel(); - handleExactMatchChange(false); - }; - }, [term, type, searchForMatches, handleExactMatchChange]); + return () => searchForMatches.cancel(); + }, [term, type, synonyms, searchForMatches, handleExactMatchChange, setTermResults]); + return ( From b45211264a4ffc56da64a3f9dde937dc7fda2375 Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Tue, 9 Sep 2025 14:05:57 +0200 Subject: [PATCH 07/11] ILEX-136 add edit term flow --- src/components/TermEditor/NewTermSidebar.jsx | 14 +- .../TermEditor/newTerm/AddNewTermDialog.jsx | 53 +++++- .../TermEditor/newTerm/FirstStepContent.jsx | 172 +++++++++--------- .../TermEditor/newTerm/SecondStepContent.jsx | 4 +- 4 files changed, 143 insertions(+), 100 deletions(-) diff --git a/src/components/TermEditor/NewTermSidebar.jsx b/src/components/TermEditor/NewTermSidebar.jsx index 37336c54..79e9c1a6 100644 --- a/src/components/TermEditor/NewTermSidebar.jsx +++ b/src/components/TermEditor/NewTermSidebar.jsx @@ -140,15 +140,18 @@ const ResultItemSkeleton = () => ( ); -const ResultItem = ({ result, searchValue, onResultAction }) => { +const ResultItem = ({ result, searchValue, onResultAction, user }) => { const isExactMatch = result.label.toLowerCase() === searchValue?.toLowerCase(); const getItemStyles = () => ({ borderBottom: `1px solid ${gray200}`, position: 'relative', + borderRadius: '0.5rem', + cursor: isExactMatch ? 'default' : 'pointer', ...(isExactMatch && EXACT_MATCH_STYLES), '&:hover': { ...(!isExactMatch && { + backgroundColor: gray200, '&:before': HOVER_INDICATOR_STYLES }) } @@ -163,6 +166,7 @@ const ResultItem = ({ result, searchValue, onResultAction }) => { px={1} py={1.5} gap={1} + onClick={() => !isExactMatch && onResultAction(result)} sx={getItemStyles()} > { height: "auto", '&:hover': { backgroundColor: "transparent" } }} - onClick={() => onResultAction(result)} + onClick={() => window.location.href = `/${user.groupname}/${result.ilx}`} > Go to term @@ -224,7 +228,8 @@ export default function NewTermSidebar({ results, isResultsEmpty, searchValue, - onResultAction + onResultAction, + user }) { const sidebarStyles = { @@ -274,6 +279,7 @@ export default function NewTermSidebar({ result={result} searchValue={searchValue} onResultAction={onResultAction} + user={user} /> ))} @@ -297,6 +303,7 @@ ResultItem.propTypes = { result: PropTypes.object.isRequired, searchValue: PropTypes.string, onResultAction: PropTypes.func, + user: PropTypes.object, }; NewTermSidebar.propTypes = { @@ -307,4 +314,5 @@ NewTermSidebar.propTypes = { isResultsEmpty: PropTypes.bool.isRequired, searchValue: PropTypes.string.isRequired, onResultAction: PropTypes.func, + user: PropTypes.object, }; \ No newline at end of file diff --git a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx index 091ccde6..db57def3 100644 --- a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx +++ b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx @@ -29,7 +29,8 @@ const HeaderRightSideContent = ({ activeStep, onContinue, onClose, - isCreateButtonDisabled + isCreateButtonDisabled, + isEditing }) => { const [ontologyChecked, setOntologyChecked] = useState(false); @@ -79,7 +80,7 @@ const HeaderRightSideContent = ({ } }} > - Create new + {isEditing ? 'Edit term' : 'Create new'} @@ -94,7 +95,8 @@ HeaderRightSideContent.propTypes = { activeStep: PropTypes.number.isRequired, onContinue: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, - isCreateButtonDisabled: PropTypes.bool.isRequired + isCreateButtonDisabled: PropTypes.bool.isRequired, + isEditing: PropTypes.bool.isRequired }; const AddNewTermDialog = ({ open, handleClose }) => { @@ -102,13 +104,15 @@ const AddNewTermDialog = ({ open, handleClose }) => { const [addTermResponse, setAddTermResponse] = useState(null); const [selectedType, setSelectedType] = useState(DEFAULT_TYPE); const [termValue, setTermValue] = useState(""); + const [selectedTermValue, setSelectedTermValue] = useState(""); const [exactSynonyms, setExactSynonyms] = useState([]); const [existingIds, setExistingIds] = useState([]); const [loading, setLoading] = useState(false); const [hasExactMatch, setHasExactMatch] = useState(false); + const [isEditing, setIsEditing] = useState(false); const { user } = useContext(GlobalDataContext); - const isCreateButtonDisabled = hasExactMatch || termValue === ""; + const isCreateButtonDisabled = hasExactMatch || termValue === "" || (isEditing && termValue === selectedTermValue); const statusProps = getAddTermStatusProps(addTermResponse, termValue); const handleCancelBtnClick = () => { @@ -116,9 +120,18 @@ const AddNewTermDialog = ({ open, handleClose }) => { setActiveStep(0); }; - const handleTermValueChange = (event) => { - const value = event.target.value; - setTermValue(value); + const handleTermValueChange = (value) => { + const isEventObject = value && typeof value === 'object' && 'target' in value; + const newValue = isEventObject ? value.target.value : value; + setTermValue(newValue); + + if (isEventObject && isEditing) { + if (newValue !== selectedTermValue) { + setIsEditing(true); + } + } else if (isEventObject) { + setIsEditing(false); + } }; const handleTypeChange = (newType) => { @@ -137,6 +150,18 @@ const AddNewTermDialog = ({ open, handleClose }) => { setHasExactMatch(value) } + const handleTermSelection = (result) => { + if (result?.label) { + setIsEditing(true); + setSelectedTermValue(result.label); + handleTermValueChange(result.label); + } + } + + const editTerm = useCallback(() => { + console.log("Edit term"); + }, []); + const createNewTerm = useCallback(async () => { if (!termValue || !selectedType || hasExactMatch) return; @@ -167,6 +192,14 @@ const AddNewTermDialog = ({ open, handleClose }) => { } }, [termValue, selectedType, user, hasExactMatch]); + const handleAction = useCallback(() => { + if (isEditing) { + editTerm(); + } else { + createNewTerm(); + } + }, [isEditing, editTerm, createNewTerm]); + if (loading) { return @@ -182,8 +215,9 @@ const AddNewTermDialog = ({ open, handleClose }) => { } sx={{ '& .MuiDialogContent-root': { padding: 0, overflowY: "hidden" } }} @@ -194,12 +228,13 @@ const AddNewTermDialog = ({ open, handleClose }) => { hasExactMatch={hasExactMatch} existingIds={existingIds} synonyms={exactSynonyms} + isEditing={isEditing} handleTermChange={handleTermValueChange} handleTypeChange={handleTypeChange} handleExactMatchChange={handleExactMatchChange} handleSynonymChange={handleSynonymChange} handleExistingIdChange={handleExistingIdChange} - handleDialogClose={handleClose} + onTermSelect={handleTermSelection} />} {activeStep === 1 && } {activeStep === 2 && addTermResponse != null && ( diff --git a/src/components/TermEditor/newTerm/FirstStepContent.jsx b/src/components/TermEditor/newTerm/FirstStepContent.jsx index 6c8640ef..d1c8f4fd 100644 --- a/src/components/TermEditor/newTerm/FirstStepContent.jsx +++ b/src/components/TermEditor/newTerm/FirstStepContent.jsx @@ -10,7 +10,6 @@ import { 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"; @@ -69,75 +68,24 @@ const styles = { } }; -const useTermSearch = (user, handleExactMatchChange) => { - const [termResults, setTermResults] = useState([]); - const [loading, setLoading] = useState(false); - - const searchForMatches = useMemo(() => - debounce(async (searchTerm, searchType, synonyms = []) => { - if (!searchTerm || !searchType) { - setTermResults([]); - handleExactMatchChange(false); - return; - } - - setLoading(true); - - try { - try { - await checkPotentialMatches(user?.groupname || 'base', { - label: searchTerm, - 'rdf-type': searchType, - exact: synonyms - }); - handleExactMatchChange(false); - } catch (error) { - if (error?.response?.status === 409 && error?.response?.data?.existing) { - handleExactMatchChange(true); - } else { - handleExactMatchChange(false); - } - } - - const { results } = await elasticSearch(searchTerm); - const similarResults = (results?.results || []).map(result => ({ - ...result, - isExactMatch: false - })); - - setTermResults(similarResults); - } catch (error) { - console.error("Term search error:", error); - setTermResults([]); - handleExactMatchChange(false); - } finally { - setLoading(false); - } - }, 500), - [user, handleExactMatchChange] - ); - - return { termResults, loading, searchForMatches, setTermResults }; -}; - const FirstStepContent = ({ term, type, hasExactMatch, existingIds, synonyms, + isEditing, handleTermChange, handleTypeChange, handleExactMatchChange, handleExistingIdChange, handleSynonymChange, - handleDialogClose + onTermSelect }) => { const [openSidebar, setOpenSidebar] = useState(true); - const { user, updateStoredSearchTerm } = useContext(GlobalDataContext); - const navigate = useNavigate(); - - const { termResults, loading, searchForMatches, setTermResults } = useTermSearch(user, handleExactMatchChange); + const [loading, setLoading] = useState(false); + const [searchResults, setSearchResults] = useState([]); + const { user } = useContext(GlobalDataContext); const synonymOptions = useMemo(() => [], []); const idOptions = useMemo(() => [], []); @@ -145,14 +93,72 @@ const FirstStepContent = ({ const handleSidebarToggle = useCallback(() => setOpenSidebar(prev => !prev), []); - const navigateToExistingTerm = useCallback((searchResult) => { - if (!searchResult?.label || !searchResult?.ilx) return; + const searchTerms = useMemo(() => { + const searchFunction = async (searchTerm, searchType, synonymList) => { + if (!searchTerm || !searchType || isEditing) { + setSearchResults([]); + return; + } - updateStoredSearchTerm(searchResult.label); - const groupName = user?.groupname || 'base'; - navigate(`/${groupName}/${searchResult.ilx}/overview`); - handleDialogClose(); - }, [updateStoredSearchTerm, user, navigate, handleDialogClose]); + setLoading(true); + try { + await checkPotentialMatches(user?.groupname || 'base', { + label: searchTerm, + 'rdf-type': searchType, + exact: synonymList + }); + // If no exact match, search elastic + handleExactMatchChange(false); + const { results } = await elasticSearch(searchTerm); + setSearchResults((results?.results || []).map(result => ({ + ...result, + isExactMatch: false + }))); + } catch (error) { + if (error?.response?.status === 409 && error?.response?.data?.existing) { + handleExactMatchChange(true); + const exactMatches = Object.entries(error.response.data.existing) + .flatMap(([termUri, matches]) => { + const matchList = Array.isArray(matches) ? matches : [matches]; + return matchList.map(match => ({ + ilx: termUri.split('/').pop(), + label: match.object, + isExactMatch: true + })); + }); + setSearchResults(exactMatches); + } else { + handleExactMatchChange(false); + setSearchResults([]); + } + } + setLoading(false); + }; + + return debounce(searchFunction, 500); + }, [user, handleExactMatchChange, isEditing, setLoading, setSearchResults]); + + const handleResultSelect = useCallback((result) => { + if (!result?.label) return; + + if (!result.isExactMatch) { + handleTermChange(result.label); + setSearchResults([]); + if (onTermSelect) { + onTermSelect({ ...result, isEditing: true }); + } + } + }, [handleTermChange, onTermSelect]); + + useEffect(() => { + if (!term) { + setSearchResults([]); + handleExactMatchChange(false); + return; + } + searchTerms(term, type, synonyms); + return () => searchTerms.cancel(); + }, [term, type, synonyms, searchTerms, handleExactMatchChange, setSearchResults]); const renderChips = useCallback((values, getTagProps, chipStyles) => values.map((option, index) => ( @@ -165,17 +171,7 @@ const FirstStepContent = ({ /> )), []); - useEffect(() => { - if (!term) { - setTermResults([]); - handleExactMatchChange(false); - return; - } - if (term && type) { - searchForMatches(term, type, synonyms); - } - return () => searchForMatches.cancel(); - }, [term, type, synonyms, searchForMatches, handleExactMatchChange, setTermResults]); + return ( @@ -259,17 +255,20 @@ const FirstStepContent = ({ - + {!isEditing && ( + + )} - ); + ) }; FirstStepContent.propTypes = { @@ -278,12 +277,13 @@ FirstStepContent.propTypes = { hasExactMatch: PropTypes.bool, existingIds: PropTypes.arrayOf(PropTypes.string), synonyms: PropTypes.arrayOf(PropTypes.string), + isEditing: PropTypes.bool, handleTermChange: PropTypes.func.isRequired, handleTypeChange: PropTypes.func.isRequired, handleExactMatchChange: PropTypes.func.isRequired, handleExistingIdChange: PropTypes.func.isRequired, handleSynonymChange: PropTypes.func.isRequired, - handleDialogClose: PropTypes.func.isRequired, + onTermSelect: PropTypes.func }; FirstStepContent.defaultProps = { diff --git a/src/components/TermEditor/newTerm/SecondStepContent.jsx b/src/components/TermEditor/newTerm/SecondStepContent.jsx index e8df731d..ca19089e 100644 --- a/src/components/TermEditor/newTerm/SecondStepContent.jsx +++ b/src/components/TermEditor/newTerm/SecondStepContent.jsx @@ -52,7 +52,7 @@ const SecondStepContent = ({ searchTerm }) => { { subject: "", predicate: "", - object: { type: "Object", value: "", isLink: false }, + object: { type: "Object", value: searchTerm, isLink: false }, }, ]) @@ -264,7 +264,7 @@ const SecondStepContent = ({ searchTerm }) => { } SecondStepContent.propTypes = { - searchTerm: PropTypes.string + searchTerm: PropTypes.string }; export default SecondStepContent; From 4ee954d8da64ed15775986672fa225a545e51eb0 Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Tue, 16 Sep 2025 16:06:51 +0200 Subject: [PATCH 08/11] ILEX-136 change post request logic and make code more readable by creating a hook --- src/api/endpoints/apiActions.ts | 67 +++++++- src/api/endpoints/apiService.ts | 81 +-------- src/components/TermEditor/NewTermSidebar.jsx | 4 +- .../TermEditor/newTerm/AddNewTermDialog.jsx | 14 +- .../TermEditor/newTerm/FirstStepContent.jsx | 162 +++++------------- src/hooks/useTermSearch.js | 90 ++++++++++ 6 files changed, 217 insertions(+), 201 deletions(-) create mode 100644 src/hooks/useTermSearch.js diff --git a/src/api/endpoints/apiActions.ts b/src/api/endpoints/apiActions.ts index 06fe6f0f..07b55ace 100644 --- a/src/api/endpoints/apiActions.ts +++ b/src/api/endpoints/apiActions.ts @@ -3,8 +3,71 @@ import { customInstance } from '../../../mock/mutator/customClient'; type SecondParameter any> = Parameters[1]; -export const createPostRequest = (endpoint: string, headers : object) => { - return (data?: D, options?: SecondParameter) => { +interface CustomRequestConfig extends AxiosRequestConfig { + handleRedirect?: boolean; +} + +export const createPostRequest = (endpoint: string, headers: object) => { + return async (data?: D, options?: CustomRequestConfig) => { + // Use fetch for handling redirect responses + if (options?.handleRedirect) { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + ...headers, + }, + body: JSON.stringify(data), + credentials: 'include', + redirect: 'manual' + }); + + // Handle 303 redirect + if (response.status === 303) { + const redirectUrl = response.headers.get('x-redirect-location') || response.headers.get('Location'); + if (redirectUrl) { + const tmpMatch = redirectUrl.match(/tmp_\d{9}/); + if (tmpMatch) { + return { + term: { + id: tmpMatch[0] + } + }; + } + } + } + + // Handle 409 Conflict + if (response.status === 409) { + const responseData = await response.json(); + const match = responseData?.existing?.[0]; + if (match) { + return { + term: { + id: match + }, + raw: responseData, + status: response.status + }; + } + } + + // Default response + const text = await response.text(); + try { + const json = JSON.parse(text); + return { + raw: json, + status: response.status + }; + } catch { + return { + raw: text, + status: response.status + }; + } + } + + // Default axios behavior for normal requests return customInstance( { url: endpoint, diff --git a/src/api/endpoints/apiService.ts b/src/api/endpoints/apiService.ts index 7ccbaf14..eb316ec9 100644 --- a/src/api/endpoints/apiService.ts +++ b/src/api/endpoints/apiService.ts @@ -137,84 +137,7 @@ export const getSelectedTermLabel = async (searchTerm: string, group: string = ' export const createNewEntity = async ({ group, data, session }: { group: string; data: any; session: string }) => { const endpoint = `/${group}${API_CONFIG.REAL_API.CREATE_NEW_ENTITY}`; - - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - - xhr.open('POST', endpoint, true); - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.withCredentials = true; - - xhr.onreadystatechange = function() { - if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) { - if (xhr.status === 303) { - const redirectUrl = xhr.getResponseHeader('x-redirect-location') || xhr.getResponseHeader('Location'); - if (redirectUrl) { - const tmpMatch = redirectUrl.match(/tmp_\d{9}/); - console.log("Found tmp ID:", tmpMatch?.[0]); - - if (tmpMatch) { - resolve({ - term: { - id: tmpMatch[0] - } - }); - return; - } - } - } - } - - if (xhr.readyState === XMLHttpRequest.DONE) { - if (xhr.status === 0) { - // Try to find tmp ID in the response text - const tmpMatch = xhr.responseText?.match(/tmp_\d{9}/); - if (tmpMatch) { - resolve({ - term: { - id: tmpMatch[0] - }, - raw: xhr.responseText, - status: 303 - }); - return; - } - } - - // Handle 409 Conflict - if (xhr.status === 409) { - try { - const responseData = JSON.parse(xhr.responseText); - const match = responseData?.existing?.[0]; - if (match) { - resolve({ - term: { - id: match - }, - raw: responseData, - status: xhr.status - }); - return; - } - } catch (e) { - console.error("Error parsing 409 response:", e); - } - } - - resolve({ - raw: xhr.responseText, - status: xhr.status - }); - } - }; - - xhr.onerror = function() { - console.error("XHR Error:", xhr.status, xhr.statusText); - reject(new Error('Network request failed')); - }; - - xhr.send(JSON.stringify(data)); - }); + return createPostRequest(endpoint, { 'Content-Type': 'application/json' })(data, { handleRedirect: true }); } export const createNewOntology = async ({ @@ -259,7 +182,7 @@ export const createNewOntology = async ({ const getResponse = await fetch(olympianRedirectLocation); const jsonResponse = await getResponse.json(); - const newOntologyID = jsonResponse?.["@graph"]?.find((object) => object["@type"] === "owl:Ontology")?.["@id"] || null; + const newOntologyID = jsonResponse?.["@graph"]?.find((object: { "@type": string; "@id": string }) => object["@type"] === "owl:Ontology")?.["@id"] || null; return { created: true, diff --git a/src/components/TermEditor/NewTermSidebar.jsx b/src/components/TermEditor/NewTermSidebar.jsx index 79e9c1a6..7339377b 100644 --- a/src/components/TermEditor/NewTermSidebar.jsx +++ b/src/components/TermEditor/NewTermSidebar.jsx @@ -1,4 +1,5 @@ import PropTypes from 'prop-types'; +import { useNavigate } from 'react-router-dom'; import { StartIcon, JoinRightIcon } from '../../Icons'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward'; @@ -142,6 +143,7 @@ const ResultItemSkeleton = () => ( const ResultItem = ({ result, searchValue, onResultAction, user }) => { const isExactMatch = result.label.toLowerCase() === searchValue?.toLowerCase(); + const navigate = useNavigate(); const getItemStyles = () => ({ borderBottom: `1px solid ${gray200}`, @@ -197,7 +199,7 @@ const ResultItem = ({ result, searchValue, onResultAction, user }) => { height: "auto", '&:hover': { backgroundColor: "transparent" } }} - onClick={() => window.location.href = `/${user.groupname}/${result.ilx}`} + onClick={() => navigate(`/${user.groupname}/${result.ilx}`)} > Go to term diff --git a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx index db57def3..115cbc7c 100644 --- a/src/components/TermEditor/newTerm/AddNewTermDialog.jsx +++ b/src/components/TermEditor/newTerm/AddNewTermDialog.jsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useContext } from "react"; +import { useState, useCallback, useContext, useMemo } from "react"; import PropTypes from "prop-types"; import { Box, @@ -112,7 +112,16 @@ const AddNewTermDialog = ({ open, handleClose }) => { const [isEditing, setIsEditing] = useState(false); const { user } = useContext(GlobalDataContext); - const isCreateButtonDisabled = hasExactMatch || termValue === "" || (isEditing && termValue === selectedTermValue); + const isCreateButtonDisabled = useMemo(() => { + if (hasExactMatch) return true; + + if (termValue === "") return true; + + if (isEditing && termValue === selectedTermValue) return true; + + return false; + }, [hasExactMatch, termValue, isEditing, selectedTermValue]); + const statusProps = getAddTermStatusProps(addTermResponse, termValue); const handleCancelBtnClick = () => { @@ -152,7 +161,6 @@ const AddNewTermDialog = ({ open, handleClose }) => { const handleTermSelection = (result) => { if (result?.label) { - setIsEditing(true); setSelectedTermValue(result.label); handleTermValueChange(result.label); } diff --git a/src/components/TermEditor/newTerm/FirstStepContent.jsx b/src/components/TermEditor/newTerm/FirstStepContent.jsx index d1c8f4fd..351a914c 100644 --- a/src/components/TermEditor/newTerm/FirstStepContent.jsx +++ b/src/components/TermEditor/newTerm/FirstStepContent.jsx @@ -1,25 +1,21 @@ import PropTypes from "prop-types"; -import { useState, useEffect, useContext, useMemo, useCallback } from "react"; -import { debounce } from "lodash"; +import { useState, useCallback, useContext } from "react"; import { Box, Divider, Stack, Typography, Autocomplete, - Chip, TextField } from "@mui/material"; 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 { checkPotentialMatches } from "../../../api/endpoints/apiService"; -import { elasticSearch } from "../../../api/endpoints"; import { vars } from "../../../theme/variables"; import { TYPES, DEFAULT_TYPE } from "../../../constants/types"; +import { useTermSearch } from "../../../hooks/useTermSearch"; const { white, gray300, gray400, gray600, gray700 } = vars; @@ -80,99 +76,34 @@ const FirstStepContent = ({ handleExactMatchChange, handleExistingIdChange, handleSynonymChange, - onTermSelect + onTermSelect, }) => { - const [openSidebar, setOpenSidebar] = useState(true); - const [loading, setLoading] = useState(false); - const [searchResults, setSearchResults] = useState([]); - const { user } = useContext(GlobalDataContext); - - const synonymOptions = useMemo(() => [], []); - const idOptions = useMemo(() => [], []); - - const handleSidebarToggle = useCallback(() => - setOpenSidebar(prev => !prev), []); - - const searchTerms = useMemo(() => { - const searchFunction = async (searchTerm, searchType, synonymList) => { - if (!searchTerm || !searchType || isEditing) { - setSearchResults([]); - return; - } - - setLoading(true); - try { - await checkPotentialMatches(user?.groupname || 'base', { - label: searchTerm, - 'rdf-type': searchType, - exact: synonymList - }); - // If no exact match, search elastic - handleExactMatchChange(false); - const { results } = await elasticSearch(searchTerm); - setSearchResults((results?.results || []).map(result => ({ - ...result, - isExactMatch: false - }))); - } catch (error) { - if (error?.response?.status === 409 && error?.response?.data?.existing) { - handleExactMatchChange(true); - const exactMatches = Object.entries(error.response.data.existing) - .flatMap(([termUri, matches]) => { - const matchList = Array.isArray(matches) ? matches : [matches]; - return matchList.map(match => ({ - ilx: termUri.split('/').pop(), - label: match.object, - isExactMatch: true - })); - }); - setSearchResults(exactMatches); - } else { - handleExactMatchChange(false); - setSearchResults([]); - } - } - setLoading(false); - }; - - return debounce(searchFunction, 500); - }, [user, handleExactMatchChange, isEditing, setLoading, setSearchResults]); - - const handleResultSelect = useCallback((result) => { - if (!result?.label) return; - - if (!result.isExactMatch) { - handleTermChange(result.label); - setSearchResults([]); + const [openSidebar, setOpenSidebar] = useState(true) + const { user } = useContext(GlobalDataContext) + + const { loading, searchResults } = useTermSearch({ + term, + type, + synonyms, + isEditing, + onExactMatchChange: handleExactMatchChange, + }) + + const handleSidebarToggle = useCallback(() => { + setOpenSidebar((prev) => !prev) + }, []) + + const handleResultSelect = useCallback( + (result) => { + if (!result?.label) return + + handleTermChange(result.label) if (onTermSelect) { - onTermSelect({ ...result, isEditing: true }); + onTermSelect(result) } - } - }, [handleTermChange, onTermSelect]); - - useEffect(() => { - if (!term) { - setSearchResults([]); - handleExactMatchChange(false); - return; - } - searchTerms(term, type, synonyms); - return () => searchTerms.cancel(); - }, [term, type, synonyms, searchTerms, handleExactMatchChange, setSearchResults]); - - const renderChips = useCallback((values, getTagProps, chipStyles) => - values.map((option, index) => ( - } - sx={chipStyles} - {...getTagProps({ index })} - /> - )), []); - - - + }, + [handleTermChange, onTermSelect], + ) return ( @@ -218,7 +149,7 @@ const FirstStepContent = ({ value={synonyms} onChange={handleSynonymChange} popupIcon={} - options={synonymOptions} + options={[]} freeSolo renderTags={(values, getTagProps) => renderChips(values, getTagProps, styles.chip.synonym)} fullWidth @@ -245,7 +176,7 @@ const FirstStepContent = ({ value={existingIds} onChange={handleExistingIdChange} popupIcon={} - options={idOptions} + options={[]} freeSolo renderTags={(values, getTagProps) => renderChips(values, getTagProps, styles.chip.id)} fullWidth @@ -255,21 +186,20 @@ const FirstStepContent = ({ - {!isEditing && ( - - )} + ) -}; +} FirstStepContent.propTypes = { term: PropTypes.string, @@ -283,15 +213,15 @@ FirstStepContent.propTypes = { handleExactMatchChange: PropTypes.func.isRequired, handleExistingIdChange: PropTypes.func.isRequired, handleSynonymChange: PropTypes.func.isRequired, - onTermSelect: PropTypes.func -}; + onTermSelect: PropTypes.func, +} FirstStepContent.defaultProps = { - term: '', + term: "", type: DEFAULT_TYPE, hasExactMatch: false, existingIds: [], synonyms: [], -}; +} -export default FirstStepContent; \ No newline at end of file +export default FirstStepContent diff --git a/src/hooks/useTermSearch.js b/src/hooks/useTermSearch.js new file mode 100644 index 00000000..aa89552e --- /dev/null +++ b/src/hooks/useTermSearch.js @@ -0,0 +1,90 @@ +import { useState, useEffect, useCallback, useMemo, useContext } from "react" +import { debounce } from "lodash" +import { GlobalDataContext } from "../contexts/DataContext" +import { checkPotentialMatches } from "../api/endpoints/apiService" +import { elasticSearch } from "../api/endpoints" + +export const useTermSearch = ({ term, type, synonyms, isEditing, onExactMatchChange }) => { + const [loading, setLoading] = useState(false) + const [searchResults, setSearchResults] = useState([]) + const [lastSearch, setLastSearch] = useState({ term: "", type: "" }) + const { user } = useContext(GlobalDataContext) + + const searchFunction = useCallback( + async (searchTerm, searchType, synonymList) => { + if (!searchTerm || !searchType || isEditing) { + setSearchResults([]) + return; + } + + // Skip duplicate searches + if (lastSearch.term === searchTerm && lastSearch.type === searchType) { + return; + } + + setLoading(true); + + try { + await checkPotentialMatches(user?.groupname || "base", { + label: searchTerm, + "rdf-type": searchType, + exact: synonymList, + }) + + // No exact matches found, search in elastic + onExactMatchChange(false) + const { results } = await elasticSearch(searchTerm) + + const searchResults = + results?.results?.map((result) => ({ + ...result, + isExactMatch: false, + })) || [] + + setSearchResults(searchResults) + } catch (error) { + if (error?.response?.status === 409 && error?.response?.data?.existing) { + // Handle exact matches + onExactMatchChange(true) + const exactMatches = Object.entries(error.response.data.existing).flatMap( + ([termUri, matches]) => { + const matchList = Array.isArray(matches) ? matches : [matches] + return matchList.map((match) => ({ + ilx: termUri.split("/").pop(), + label: match.object ?? termUri.split("/").pop() ?? "Unknown", + isExactMatch: true, + })) + }, + ) + setSearchResults(exactMatches) + } else { + onExactMatchChange(false) + setSearchResults([]) + } + } finally { + setLoading(false) + setLastSearch({ term: searchTerm, type: searchType }) + } + }, + [user, onExactMatchChange, isEditing, lastSearch], + ) + + const debouncedSearch = useMemo(() => debounce(searchFunction, 500), [searchFunction]) + + useEffect(() => { + if (!term) { + setSearchResults([]) + onExactMatchChange(false) + return + } + + debouncedSearch(term, type, synonyms) + return () => debouncedSearch.cancel() + }, [term, type, synonyms, debouncedSearch, onExactMatchChange]) + + return { + loading, + searchResults, + clearResults: () => setSearchResults([]), + } +} From 200f5ab070f97fcc863a5135454f9ec401cfd3ce Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Wed, 17 Sep 2025 12:16:44 +0200 Subject: [PATCH 09/11] ILEX-136 put limit to elastic search --- src/hooks/useTermSearch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useTermSearch.js b/src/hooks/useTermSearch.js index aa89552e..0f8d114c 100644 --- a/src/hooks/useTermSearch.js +++ b/src/hooks/useTermSearch.js @@ -33,7 +33,7 @@ export const useTermSearch = ({ term, type, synonyms, isEditing, onExactMatchCha // No exact matches found, search in elastic onExactMatchChange(false) - const { results } = await elasticSearch(searchTerm) + const { results } = await elasticSearch(searchTerm, 30, 0) const searchResults = results?.results?.map((result) => ({ From 69ce8bcefc8f0bd31a1bb63b1952fe939832606b Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Wed, 17 Sep 2025 12:22:44 +0200 Subject: [PATCH 10/11] ILEX-136 fix linting --- .../TermEditor/newTerm/FirstStepContent.jsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/TermEditor/newTerm/FirstStepContent.jsx b/src/components/TermEditor/newTerm/FirstStepContent.jsx index 351a914c..cbaf270d 100644 --- a/src/components/TermEditor/newTerm/FirstStepContent.jsx +++ b/src/components/TermEditor/newTerm/FirstStepContent.jsx @@ -6,13 +6,15 @@ import { Stack, Typography, Autocomplete, - TextField + TextField, + Chip } from "@mui/material"; import { GlobalDataContext } from "../../../contexts/DataContext"; import CustomSingleSelect from "../../common/CustomSingleSelect"; import CustomFormField from "../../common/CustomFormField"; import NewTermSidebar from "../NewTermSidebar"; import { HelpOutlinedIcon } from "../../../Icons"; +import CloseIcon from "@mui/icons-material/Close"; import { vars } from "../../../theme/variables"; import { TYPES, DEFAULT_TYPE } from "../../../constants/types"; import { useTermSearch } from "../../../hooks/useTermSearch"; @@ -105,6 +107,17 @@ const FirstStepContent = ({ [handleTermChange, onTermSelect], ) + const renderChips = useCallback((values, getTagProps, chipStyles) => + values.map((option, index) => ( + } + sx={chipStyles} + {...getTagProps({ index })} + /> + )), []); + return ( From 32b4f669e817cb1e07f3e92181f3fcced8506355 Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Fri, 19 Sep 2025 12:45:54 +0200 Subject: [PATCH 11/11] ILEX-136 delete unnecessary logic --- src/api/endpoints/apiActions.ts | 32 +------------------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/src/api/endpoints/apiActions.ts b/src/api/endpoints/apiActions.ts index 07b55ace..2378e1f6 100644 --- a/src/api/endpoints/apiActions.ts +++ b/src/api/endpoints/apiActions.ts @@ -21,37 +21,7 @@ export const createPostRequest = (endpoint: string, headers: o redirect: 'manual' }); - // Handle 303 redirect - if (response.status === 303) { - const redirectUrl = response.headers.get('x-redirect-location') || response.headers.get('Location'); - if (redirectUrl) { - const tmpMatch = redirectUrl.match(/tmp_\d{9}/); - if (tmpMatch) { - return { - term: { - id: tmpMatch[0] - } - }; - } - } - } - - // Handle 409 Conflict - if (response.status === 409) { - const responseData = await response.json(); - const match = responseData?.existing?.[0]; - if (match) { - return { - term: { - id: match - }, - raw: responseData, - status: response.status - }; - } - } - - // Default response + // Default response handling const text = await response.text(); try { const json = JSON.parse(text);