From 03d8cd30e00bf09f1a21a374519427be2a98d2eb Mon Sep 17 00:00:00 2001 From: Dario Del Piano Date: Thu, 9 Jul 2026 13:55:58 +0200 Subject: [PATCH] fix groupname in uri, predicates title to use curie if available, nginx proxy server fix --- nginx/default.conf | 16 ++++--------- src/api/endpoints/apiService.ts | 21 ++++++++++++---- .../SingleTermView/OverView/OverView.jsx | 24 +++++++++++-------- src/components/SingleTermView/index.jsx | 13 +++++++--- src/configuration/predicateConfig.ts | 20 ++++++++++++++-- src/hooks/useTermData.js | 8 +++++-- 6 files changed, 69 insertions(+), 33 deletions(-) diff --git a/nginx/default.conf b/nginx/default.conf index 3277ba3..abb3c63 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -208,23 +208,17 @@ server { return 200 '{"location":"$upstream_http_location"}'; } - # Handle 303 redirects for spec endpoint + # Convert spec-endpoint 303 → 200 + JSON body with redirect location + # (no second proxy_pass here - that resent the original POST to the + # upstream a second time, which nginx's 303 interception already saw) location @handle_303 { internal; - proxy_pass https://uri.olympiangods.org; - proxy_set_header Host uri.olympiangods.org; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Authorization $http_authorization; - proxy_ssl_verify off; - - # Move Location header to X-Redirect-Location - proxy_hide_header Location; + default_type application/json; add_header X-Redirect-Location $upstream_http_location always; add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Credentials true always; add_header Access-Control-Expose-Headers X-Redirect-Location always; + return 200 '{"location":"$upstream_http_location"}'; } location /static/ { diff --git a/src/api/endpoints/apiService.ts b/src/api/endpoints/apiService.ts index c57c7f5..0ca6985 100644 --- a/src/api/endpoints/apiService.ts +++ b/src/api/endpoints/apiService.ts @@ -59,12 +59,21 @@ type LabelType = interface GraphNode { 'rdfs:label'?: LabelType; + 'owl:versionIRI'?: { '@id'?: string }; } interface JsonLdResponse { '@graph'?: GraphNode[]; } +// owl:versionIRI is normally a full IRI (.../version//...); pull out the +// bare identity-graph id, mirroring Details.jsx's versionDisplay logic. +const extractGraphId = (graph?: GraphNode[]): string | undefined => { + const versionIRI = graph?.[graph.length - 1]?.['owl:versionIRI']?.['@id']; + if (!versionIRI) return undefined; + return versionIRI.includes('/version/') ? versionIRI.split('/version/')[1]?.split('/')[0] : versionIRI; +}; + const BASE_EXTENSION = "jsonld"; // Deduplicate concurrent GET fetches for the same URL. @@ -170,7 +179,7 @@ export const changePassword = (group: string, data: { username: string; currentP return createPostRequest(endpoint, { "Content-Type": "application/x-www-form-urlencoded" })(data); }; -export const getSelectedTermLabel = async (searchTerm: string, group: string = 'base'): Promise<{ label: string | undefined; actualGroup: string }> => { +export const getSelectedTermLabel = async (searchTerm: string, group: string = 'base'): Promise<{ label: string | undefined; actualGroup: string; graphId: string | undefined }> => { try { const primaryUrl = `/${group}/${searchTerm}.jsonld`; const response = await fetchOnce(primaryUrl, () => createGetRequest(primaryUrl)()); @@ -190,7 +199,8 @@ export const getSelectedTermLabel = async (searchTerm: string, group: string = ' return { label: label ? getLabelValue(label) : undefined, - actualGroup: group + actualGroup: group, + graphId: extractGraphId(response['@graph']) }; } catch (err: any) { console.error(err.message); @@ -214,14 +224,15 @@ export const getSelectedTermLabel = async (searchTerm: string, group: string = ' return { label: fallbackLabel ? getLabelValue(fallbackLabel) : undefined, - actualGroup: 'base' + actualGroup: 'base', + graphId: extractGraphId(fallbackResponse['@graph']) }; } catch (fallbackErr: any) { console.error('Fallback request also failed:', fallbackErr.message); - return { label: undefined, actualGroup: group }; + return { label: undefined, actualGroup: group, graphId: undefined }; } } - return { label: undefined, actualGroup: group }; + return { label: undefined, actualGroup: group, graphId: undefined }; } }; diff --git a/src/components/SingleTermView/OverView/OverView.jsx b/src/components/SingleTermView/OverView/OverView.jsx index 1b10b8b..61997a1 100644 --- a/src/components/SingleTermView/OverView/OverView.jsx +++ b/src/components/SingleTermView/OverView/OverView.jsx @@ -66,12 +66,13 @@ const toILX = (curieLike) => { return t.replace(/^ilx_/i, "ILX:"); }; -// Normalize a predicate group's title + row predicates from full IRIs to curies. -const shortenGroup = (g) => ({ +// Normalize a predicate group's title + row predicates from full IRIs to +// curies, preferring the app's live (user/org) curies over the hardcoded ones. +const shortenGroup = (g, curies) => ({ ...g, - title: shortenIri(g.title), + title: shortenIri(g.title, curies), tableData: Array.isArray(g.tableData) - ? g.tableData.map((r) => ({ ...r, predicate: shortenIri(r.predicate) })) + ? g.tableData.map((r) => ({ ...r, predicate: shortenIri(r.predicate, curies) })) : g.tableData, }); @@ -83,8 +84,9 @@ const shortenGroup = (g) => ({ // backend serves head-consistent data, the editable literal predicates are // overridden with the fresh .jsonld groups. Once fixed, return // dedupePredicateGroups(predicateGroups.map(shortenGroup)). -const mergePredicates = ({ versionHash, jsonData, predicateGroups, focusCurie, searchTerm }) => { +const mergePredicates = ({ versionHash, jsonData, predicateGroups, focusCurie, searchTerm, curies }) => { const termId = focusCurie ? toILX(focusCurie) : searchTerm; + const shorten = (g) => shortenGroup(g, curies); if (versionHash) { if (!jsonData) return []; @@ -93,19 +95,19 @@ const mergePredicates = ({ versionHash, jsonData, predicateGroups, focusCurie, s return dedupePredicateGroups( buildPredicateGroupsForFocus(jsonData, termId) .filter((g) => !META_TITLES.has(norm(g.title))) - .map(shortenGroup) + .map(shorten) ); } const freshGroups = jsonData - ? buildPredicateGroupsForFocus(jsonData, termId).map(shortenGroup) + ? buildPredicateGroupsForFocus(jsonData, termId).map(shorten) : []; const freshLiteralTitles = new Set( freshGroups.filter((g) => getObjectInputKind(g.title) === "text").map((g) => norm(g.title)) ); const freshLiterals = freshGroups.filter((g) => freshLiteralTitles.has(norm(g.title))); const transitive = (Array.isArray(predicateGroups) ? predicateGroups : []) - .map(shortenGroup) + .map(shorten) .filter((g) => !freshLiteralTitles.has(norm(g.title))); return dedupePredicateGroups([...freshLiterals, ...transitive]); @@ -183,10 +185,11 @@ const OverView = ({ searchTerm, isCodeViewVisible = false, selectedDataFormat, g predicateGroups: groupsRef.current, focusCurie: focusId, searchTerm, + curies: curies?.base, }); store.predicates$.next({ loading: false, data, focusId }); }, - [store, searchTerm] + [store, searchTerm, curies] ); // Live (head) load. Each fetch resolves and writes its own stream. @@ -376,6 +379,7 @@ const OverView = ({ searchTerm, isCodeViewVisible = false, selectedDataFormat, g predicateGroups: [], focusCurie: sv.id, searchTerm, + curies: curies?.base, }); store.predicates$.next({ loading: false, data, focusId: sv.id }); } catch (e) { @@ -392,7 +396,7 @@ const OverView = ({ searchTerm, isCodeViewVisible = false, selectedDataFormat, g // eslint-disable-next-line react-hooks/exhaustive-deps loadTokenRef.current++; }; - }, [versionHash, searchTerm, group, store]); + }, [versionHash, searchTerm, group, store, curies]); const handleSelect = useCallback( (value) => { diff --git a/src/components/SingleTermView/index.jsx b/src/components/SingleTermView/index.jsx index 83cede4..dac04d3 100644 --- a/src/components/SingleTermView/index.jsx +++ b/src/components/SingleTermView/index.jsx @@ -54,7 +54,7 @@ import { reportApiError } from "../../api/apiErrorBus"; import ApiErrorDialog from "../common/ApiErrorDialog"; import { useTermData } from "../../hooks/useTermData"; -const { gray200, gray600, error700 } = vars; +const { gray200, gray500, gray600, error700 } = vars; // Pull the InterLex id (ilx_/tmp_) out of an arbitrary IRI/string for membership checks. const extractIlxId = (value) => { @@ -109,7 +109,7 @@ const SingleTermView = () => { const [featureNotAvailableDialog, setFeatureNotAvailableDialog] = useState(false); // Use the optimized term data hook instead of manual fetching - const { termData, actualGroup, isUsingFallback, isLoadingTerm } = useTermData(term, group); + const { termData, actualGroup, graphId, isUsingFallback, isLoadingTerm } = useTermData(term, group); const [versionsData, setVersionsData] = useState(null); const [versionsLoading, setVersionsLoading] = useState(true); @@ -481,7 +481,14 @@ const SingleTermView = () => { - + + + {graphId && ( + + Graph ID: {graphId} + + )} + diff --git a/src/configuration/predicateConfig.ts b/src/configuration/predicateConfig.ts index bff8375..5ac488c 100644 --- a/src/configuration/predicateConfig.ts +++ b/src/configuration/predicateConfig.ts @@ -98,9 +98,25 @@ export const buildExpandContext = ( return ctx; }; -// Shorten a full predicate IRI to its curie; pass through curies/non-IRIs. -export const shortenIri = (iri: string): string => { +// Shorten a full predicate IRI to its curie. Checks the app's live curies +// (user/org-registered namespaces) first, then the hardcoded tables above; +// pass through curies/non-IRIs/unmatched IRIs unchanged. +export const shortenIri = ( + iri: string, + curies: Array<{ prefix: string; namespace: string }> = [] +): string => { if (!iri || !/^https?:\/\//i.test(iri)) return iri; + + let bestNamespace = ""; + let curieMatch = ""; + for (const { prefix, namespace } of curies) { + if (namespace && iri.startsWith(namespace) && namespace.length > bestNamespace.length) { + bestNamespace = namespace; + curieMatch = `${prefix}:${iri.slice(namespace.length)}`; + } + } + if (curieMatch) return curieMatch; + if (KNOWN_TERMS[iri]) return KNOWN_TERMS[iri]; let best = ""; let curie = iri; diff --git a/src/hooks/useTermData.js b/src/hooks/useTermData.js index 03d959f..18894a0 100644 --- a/src/hooks/useTermData.js +++ b/src/hooks/useTermData.js @@ -12,13 +12,14 @@ export const primeTermDataCache = (group, term, label) => { if (!group || !term || !label) return; const cacheKey = `${group}:${term}`; if (!termDataCache.has(cacheKey)) { - termDataCache.set(cacheKey, { label, actualGroup: group }); + termDataCache.set(cacheKey, { label, actualGroup: group, graphId: undefined }); } }; export const useTermData = (searchTerm, group) => { const [termData, setTermData] = useState(null); const [actualGroup, setActualGroup] = useState(group); + const [graphId, setGraphId] = useState(undefined); const [isUsingFallback, setIsUsingFallback] = useState(false); const [isLoadingTerm, setIsLoadingTerm] = useState(false); const abortControllerRef = useRef(null); @@ -28,12 +29,13 @@ export const useTermData = (searchTerm, group) => { // Create cache key const cacheKey = `${groupName}:${term}`; - + // Check cache first if (termDataCache.has(cacheKey)) { const cachedData = termDataCache.get(cacheKey); setTermData(cachedData.label); setActualGroup(cachedData.actualGroup); + setGraphId(cachedData.graphId); setIsUsingFallback(cachedData.actualGroup !== groupName); return; } @@ -61,6 +63,7 @@ export const useTermData = (searchTerm, group) => { setTermData(result.label); setActualGroup(result.actualGroup); + setGraphId(result.graphId); setIsUsingFallback(result.actualGroup !== groupName); } catch (error) { if (error.name !== 'AbortError') { @@ -86,6 +89,7 @@ export const useTermData = (searchTerm, group) => { return { termData, actualGroup, + graphId, isUsingFallback, isLoadingTerm, refreshTermData: () => {