Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions nginx/default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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/ {
Expand Down
21 changes: 16 additions & 5 deletions src/api/endpoints/apiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>/...); 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.
Expand Down Expand Up @@ -170,7 +179,7 @@ export const changePassword = (group: string, data: { username: string; currentP
return createPostRequest<any, any>(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<JsonLdResponse, any>(primaryUrl)());
Expand All @@ -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);
Expand All @@ -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 };
}
};

Expand Down
24 changes: 14 additions & 10 deletions src/components/SingleTermView/OverView/OverView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand All @@ -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 [];
Expand All @@ -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]);
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -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) => {
Expand Down
13 changes: 10 additions & 3 deletions src/components/SingleTermView/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -481,7 +481,14 @@ const SingleTermView = () => {
</Stack>
</Grid>
<Grid item xs={6}>
<CopyLinkComponent url={`http://uri.interlex.org/${searchTerm?.startsWith('ilx_') ? 'base' : actualGroup}/${searchTerm}`} />
<Stack direction="row" spacing="1rem" alignItems="center">
<CopyLinkComponent url={`http://uri.interlex.org/${actualGroup}/${searchTerm}`} />
{graphId && (
<Typography fontSize=".875rem" color={gray500}>
Graph ID: {graphId}
</Typography>
)}
</Stack>
</Grid>
<Grid item xs={12} mt="2rem" display='flex' alignItems='center' justifyContent='space-between'>
<BasicTabs tabValue={tabValue} handleChange={handleChangeTabs} tabs={tabLabels} />
Expand Down
20 changes: 18 additions & 2 deletions src/configuration/predicateConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions src/hooks/useTermData.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down Expand Up @@ -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') {
Expand All @@ -86,6 +89,7 @@ export const useTermData = (searchTerm, group) => {
return {
termData,
actualGroup,
graphId,
isUsingFallback,
isLoadingTerm,
refreshTermData: () => {
Expand Down
Loading