diff --git a/nginx/default.conf b/nginx/default.conf index 52ba2595..a048d664 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -125,4 +125,32 @@ server { autoindex on; alias /usr/share/nginx/html/static/; } + + # Proxy for transitive query endpoint + location ~ ^/[^/]+/query/transitive/.* { + 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; + + # Pass through auth/cookies if present (mirrors Vite configure hook) + proxy_set_header Authorization $http_authorization; + proxy_set_header Cookie $http_cookie; + + proxy_ssl_verify off; + + # Helpful CORS for credentialed requests in dev + 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; + + # (Optional but handy for preflight) + if ($request_method = OPTIONS) { + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; + add_header Access-Control-Allow-Headers $http_access_control_request_headers always; + return 204; + } + } + } diff --git a/src/api/endpoints/apiService.ts b/src/api/endpoints/apiService.ts index 8475050e..acbd1280 100644 --- a/src/api/endpoints/apiService.ts +++ b/src/api/endpoints/apiService.ts @@ -1,6 +1,7 @@ import { createPostRequest, createGetRequest } from "./apiActions"; import { API_CONFIG } from "../../config"; import termParser from "../../parsers/termParser"; +import { jsonldToTriplesAndEdges, PART_OF_IRI } from './hiearchies-parser' export interface LoginRequest { username: string @@ -300,3 +301,30 @@ export const getTermDiscussions = async (group: string, variantID: string) => { export const getVariant = (group: string, term: string) => { return createGetRequest(`/${group}/variant/${term}`, "application/json")(); }; + +export const getTermHierarchies = async ({ + groupname, + termId, + objToSub = true, +}: { + groupname: string; + termId: string; + objToSub?: boolean; +}) => { + const base = `/${groupname}/query/transitive/${encodeURIComponent(termId)}/ilx.partOf:`; + const url1 = `${base}?obj-to-sub=${objToSub}`; + const url2 = `${base}.jsonld?obj-to-sub=${objToSub}`; + + try { + const res1 = await createGetRequest(url1, 'application/ld+json')(); + return jsonldToTriplesAndEdges(res1); + } catch { + try { + const res2 = await createGetRequest(url2, 'application/ld+json')(); + return jsonldToTriplesAndEdges(res2); + } catch (e2: any) { + console.error('getTermHierarchies failed', e2); + return { error: true, message: e2?.message || String(e2) }; + } + } +}; diff --git a/src/api/endpoints/hiearchies-parser.ts b/src/api/endpoints/hiearchies-parser.ts new file mode 100644 index 00000000..4ae132dd --- /dev/null +++ b/src/api/endpoints/hiearchies-parser.ts @@ -0,0 +1,85 @@ +// Minimal JSON-LD → { triples, edges } converter used by getTermHierarchies + +export const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'; +export const RDFS_LABEL = 'http://www.w3.org/2000/01/rdf-schema#label'; +export const PART_OF_IRI = 'http://uri.interlex.org/base/ilx_0112785'; + +type JsonLdNode = { + '@id': string; + '@type'?: string[] | string; + [k: string]: any; +}; + +function firstString(o: any): string | undefined { + if (o == null) return; + if (typeof o === 'string') return o; + if (Array.isArray(o)) return firstString(o[0]); + if (typeof o === 'object') { + if ('@value' in o) return String(o['@value']); + if ('@id' in o) return String(o['@id']); + } +} + +function ids(objs: any): string[] { + if (!objs) return []; + const arr = Array.isArray(objs) ? objs : [objs]; + return arr.map(v => (typeof v === 'string' ? v : v?.['@id'])).filter(Boolean); +} + +export type Triple = { + subject: { id: string; label: string }; + predicate: { id: string; label: string }; + object: { id: string; label: string }; +}; + +export type Edge = { + from: { id: string; label: string }; + to: { id: string; label: string }; +}; + +export function jsonldToTriplesAndEdges(jsonld: any): { triples: Triple[]; edges: Edge[] } { + const graph: JsonLdNode[] = + Array.isArray(jsonld) ? jsonld : + Array.isArray(jsonld?.['@graph']) ? jsonld['@graph'] : + jsonld?.['@id'] ? [jsonld] : []; + + // id → label map (prefer rdfs:label) + const labelById = new Map(); + for (const n of graph) { + const id = n['@id']; if (!id) continue; + const lbl = firstString(n['label']) ?? firstString(n['rdfs:label']) ?? firstString(n[RDFS_LABEL]); + if (lbl) labelById.set(id, lbl); + } + + const triples: Triple[] = []; + const edges: Edge[] = []; + + const addTriple = (s: string, p: string, oId?: string, oLabel?: string) => { + const subj = { id: s, label: labelById.get(s) ?? s }; + const pred = { id: p, label: p }; + const obj = oId ? { id: oId, label: labelById.get(oId) ?? oId } : { id: '', label: oLabel ?? '' }; + triples.push({ subject: subj, predicate: pred, object: obj }); + }; + + for (const n of graph) { + const s = n['@id']; if (!s) continue; + + // rdf:type + for (const t of ids(n['@type'])) addTriple(s, RDF_TYPE, t); + + // rdfs:label + const lbl = firstString(n['label']) ?? firstString(n['rdfs:label']) ?? firstString(n[RDFS_LABEL]); + if (lbl) addTriple(s, RDFS_LABEL, undefined, lbl); + + // ilx.partOf (accept compact or expanded) + for (const o of [...ids(n['partOf']), ...ids(n[PART_OF_IRI])]) { + addTriple(s, PART_OF_IRI, o); + edges.push({ + from: { id: s, label: labelById.get(s) ?? s }, + to: { id: o, label: labelById.get(o) ?? o }, + }); + } + } + + return { triples, edges }; +} diff --git a/src/components/GraphViewer/Graph.jsx b/src/components/GraphViewer/Graph.jsx index eb5aeb1e..aa5472f6 100644 --- a/src/components/GraphViewer/Graph.jsx +++ b/src/components/GraphViewer/Graph.jsx @@ -1,180 +1,146 @@ import * as d3 from "d3"; import PropTypes from "prop-types"; import { Box } from "@mui/material"; -import { useMemo, useEffect } from "react"; -import { getGraphStructure, PREDICATE, ROOT} from "./GraphStructure"; +import { useMemo, useEffect, useCallback } from "react"; +import { getGraphStructure, PREDICATE, ROOT } from "./GraphStructure"; import { vars } from "../../theme/variables"; const { gray600, white } = vars; const MARGIN = { top: 60, right: 60, bottom: 60, left: 60 }; const Graph = ({ width, height, predicate }) => { - const boundsWidth = width - (MARGIN.right + MARGIN.left * 4); - const boundsHeight = height - MARGIN.top - MARGIN.bottom; + const boundsWidth = Math.max(0, width - (MARGIN.right + MARGIN.left * 4)); + const boundsHeight = Math.max(0, height - MARGIN.top - MARGIN.bottom); - // Three function that change the tooltip when user hover / move / leave a cell - const mouseover = (event) => { - d3.select("#tooltip") - .html(event.currentTarget.id) - .style("opacity", 1) - d3.select("#tooltip") - } - const mousemove = (event) => { + const mouseover = useCallback((event) => { + d3.select("#tooltip").html(event.currentTarget.id).style("opacity", 1); + }, []); + const mousemove = useCallback((event) => { d3.select("#tooltip") .style("left", `${event.clientX + 10}px`) .style("top", `${event.clientY + 10}px`); - } - // eslint-disable-next-line no-unused-vars - const mouseleave = (d) => { - d3.select("#tooltip").style("opacity", 0) - } - - useEffect(() => { - // attach mouse listeners - const nodes = d3.selectAll(".node--leaf-g"); - nodes - .on("mouseover", mouseover) - .on("mousemove", mousemove) - .on("mouseleave", mouseleave); - - // Hide tooltip on scroll - window.addEventListener("scroll", () => { - d3.select("#tooltip").style("opacity", 0); - }); - - return () => { - nodes.on("mouseover", null).on("mousemove", null).on("mouseleave", null); - window.removeEventListener("scroll", () => {}); - }; + }, []); + const mouseleave = useCallback(() => { + d3.select("#tooltip").style("opacity", 0); + }, []); + const onScroll = useCallback(() => { + d3.select("#tooltip").style("opacity", 0); }, []); const hierarchy = useMemo(() => { - const data = getGraphStructure(predicate) - return d3.hierarchy(data).sum((d) => d.value); + const data = getGraphStructure(predicate); + return d3.hierarchy(data).sum((d) => d.value || 1); }, [predicate]); const dendrogram = useMemo(() => { - const dendrogramGenerator = d3.cluster().size([boundsHeight, boundsWidth]); - return dendrogramGenerator(hierarchy); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hierarchy, width, height]); + const gen = d3.cluster().size([boundsHeight, boundsWidth]); + return gen(hierarchy); + }, [hierarchy, boundsHeight, boundsWidth]); + + useEffect(() => { + const nodes = d3.selectAll(".node--leaf-g"); + nodes.on("mouseover", mouseover).on("mousemove", mousemove).on("mouseleave", mouseleave); + window.addEventListener("scroll", onScroll); + return () => { + nodes.on("mouseover", null).on("mousemove", null).on("mouseleave", null); + window.removeEventListener("scroll", onScroll); + }; + }, [predicate, mouseover, mousemove, mouseleave, onScroll]); const allNodes = dendrogram.descendants().map((node) => { - let textOffset = 0; - if (node.data.type === PREDICATE || node.data.type === ROOT) { - textOffset = -40; - } else { - textOffset = 5; - } - - const truncatedName = node.data.name.length > 25 - ? `${node.data.name.substring(0, 25)}...` - : node.data.name; - + const isGroup = node.data.type === PREDICATE || node.data.type === ROOT; + const textOffset = isGroup ? -40 : 5; + const label = String(node.data.name ?? "unknown"); + const truncated = label.length > 25 ? `${label.slice(0, 25)}...` : label; + return ( - + - {truncatedName} + {truncated} ); }); - const allEdges = dendrogram.descendants().map((node, index) => { + const allEdges = dendrogram.descendants().map((node, i) => { if (!node.parent) { - // Add a black circle at the root - return ( - - ); + return ; } - - const line = d3 - .line() - .x(d => d[0]) - .y(d => d[1]) - .curve(d3.curveBundle.beta(0.75)); - + const line = d3.line().x((d) => d[0]).y((d) => d[1]).curve(d3.curveBundle.beta(0.75)); const start = [node.parent.y, node.parent.x]; const end = [node.y, node.x]; - const radius = 5; - - const points = [ - start, - [start[0] - radius, end[1]], - end, - ]; - + const points = [start, [start[0] - 5, end[1]], end]; return ( ); }); + const hasChildren = + hierarchy && hierarchy.data && Array.isArray(hierarchy.data.children) && hierarchy.data.children.length > 0; + return ( - - - - - - - - - + + + + + + + + - {allNodes} - {allEdges} + {hasChildren ? ( + <> + {allNodes} + {allEdges} + + ) : ( + // fallback: show root only with a hint + + No graph data + + )} - - ); }; @@ -182,7 +148,14 @@ const Graph = ({ width, height, predicate }) => { Graph.propTypes = { width: PropTypes.number.isRequired, height: PropTypes.number.isRequired, - predicate: PropTypes.string.isRequired, + // IMPORTANT: now expects the predicate GROUP object (with rows/values/edges or legacy tableData) + predicate: PropTypes.shape({ + title: PropTypes.string, + rows: PropTypes.array, + values: PropTypes.array, + edges: PropTypes.array, + tableData: PropTypes.array, + }).isRequired, }; export default Graph; diff --git a/src/components/GraphViewer/GraphStructure.jsx b/src/components/GraphViewer/GraphStructure.jsx index f6090c86..99eb007e 100644 --- a/src/components/GraphViewer/GraphStructure.jsx +++ b/src/components/GraphViewer/GraphStructure.jsx @@ -3,58 +3,108 @@ export const PREDICATE = "predicate"; export const SUBJECT = "subject"; export const ROOT = "root"; -// TODO : Temporary until we get real data for predicates, right now parsing to make URLs -// fit on Graph -const getName = (nodeName) => { - let name = nodeName +// tiny helpers +const safe = (v, fallback = "unknown") => { + if (v == null) return fallback; + const s = String(v).trim(); + return s.length ? s : fallback; +}; - if ( name == undefined ) { - return "name"; +// Normalize predicate group into a rows array: +// [{ subject, subjectId, object, objectId }] +function normalizeRows(pred) { + if (!pred) return []; + + // 1) new shape from getTermHierarchies grouping + if (Array.isArray(pred.rows) && pred.rows.length) return pred.rows; + if (Array.isArray(pred.values) && pred.values.length) return pred.values; + + // 2) legacy tableData + if (Array.isArray(pred.tableData) && pred.tableData.length) { + return pred.tableData.map(r => ({ + subject: r.subject, + subjectId: r.subject, // no id in legacy, use label + object: r.object, + objectId: r.object, + })); + } + + // 3) edges fallback + if (Array.isArray(pred.edges) && pred.edges.length) { + return pred.edges.map(e => ({ + subject: e?.from?.label || e?.from?.id, + subjectId: e?.from?.id || e?.from?.label, + object: e?.to?.label || e?.to?.id, + objectId: e?.to?.id || e?.to?.label, + })); } - return name; + return []; +} + +// Pick a root subject (most frequent). Fallback to first row. +function pickRoot(rows) { + if (!rows.length) return { key: "unknown", label: "unknown" }; + const counts = new Map(); + const firstLabelById = new Map(); + for (const r of rows) { + const id = r.subjectId || r.subject; + if (!id) continue; + counts.set(id, (counts.get(id) || 0) + 1); + if (!firstLabelById.has(id)) firstLabelById.set(id, r.subject || r.subjectId || id); + } + let rootKey = null, max = -1; + for (const [k, v] of counts.entries()) { + if (v > max) { max = v; rootKey = k; } + } + if (!rootKey) { + const r0 = rows[0]; + const id = r0.subjectId || r0.subject || "unknown"; + return { key: id, label: r0.subject || id }; + } + return { key: rootKey, label: firstLabelById.get(rootKey) || rootKey }; } +// Build Root → Predicate → Unique Objects export const getGraphStructure = (pred) => { - let data = { - name : pred?.tableData[0]?.subject, - id : pred?.tableData[0]?.subject, - type : ROOT, - value : pred.count, - children : [] + const rows = normalizeRows(pred); + if (!rows.length) { + return { name: "No data", id: "no-data", type: ROOT, value: 0, children: [] }; } - let uniqueObjects = []; - - pred?.tableData?.forEach( child => { - let newChild = { name : getName(child.object), id : child.object, type : OBJECT}; - - let getExistingObject = uniqueObjects?.find( c => c.id === child.object ); - if ( getExistingObject ) { - // Object already exists, just add it to the predicate's children - let getExistingPredicate = data.children?.find( c => c.id === child.predicate ); - if ( getExistingPredicate ) { - getExistingPredicate.children.push(newChild) - } - } else { - // New object, add it to uniqueObjects and process normally - uniqueObjects.push(newChild); - - let getExistingPredicate = data?.children?.find( c => c.id === child.predicate ); - if ( getExistingPredicate ) { - getExistingPredicate.children.push(newChild) - } else { - let newPredicate = { - name : getName(child.predicate), - id : child.predicate, - type : PREDICATE, - children : [newChild] - } - - data.children.push(newPredicate) - } - } - }) - - return data; -} + const { key: rootKey, label: rootLabel } = pickRoot(rows); + const forRoot = rows.filter(r => (r.subjectId || r.subject) === rootKey); + + const predicateLabel = safe(pred?.title, "predicate"); + const predicateId = predicateLabel; + + const seen = new Set(); + const objects = []; + for (const r of forRoot) { + const oid = r.objectId || r.object; + if (!oid) continue; + if (seen.has(oid)) continue; + seen.add(oid); + objects.push({ + name: safe(r.object), + id: safe(oid), + type: OBJECT, + children: [], + }); + } + + return { + name: safe(rootLabel), + id: safe(rootKey), + type: ROOT, + value: forRoot.length || pred?.count || 0, + children: [ + { + name: predicateLabel, + id: predicateId, + type: PREDICATE, + children: objects, + }, + ], + }; +}; diff --git a/src/components/SingleTermView/OverView/CustomizedTable.jsx b/src/components/SingleTermView/OverView/CustomizedTable.jsx index fc0fbec3..2b021410 100644 --- a/src/components/SingleTermView/OverView/CustomizedTable.jsx +++ b/src/components/SingleTermView/OverView/CustomizedTable.jsx @@ -7,10 +7,9 @@ import { getMatchTerms } from "../../../api/endpoints"; import { Box, IconButton, Typography } from "@mui/material"; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import AddOutlinedIcon from '@mui/icons-material/AddOutlined'; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState, useContext } from "react"; import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; import { GlobalDataContext } from "../../../contexts/DataContext"; -import { useContext } from "react"; import { vars } from "../../../theme/variables"; const { gray100, gray50, gray600, gray500, brand600, brand50, brand700, gray700 } = vars; @@ -20,16 +19,9 @@ const tableStyles = { display: 'flex', p: '0.75rem 0.5rem 0.5rem 0.5rem', borderBottom: `1px solid ${gray100}`, - - '& > .MuiBox-root': { - paddingRight: '0.75rem', - paddingLeft: 0 - }, + '& > .MuiBox-root': { paddingRight: '0.75rem', paddingLeft: 0 }, '& .MuiTypography-root': { - color: gray600, - fontWeight: 500, - fontSize: '.75rem', - lineHeight: '1.125rem' + color: gray600, fontWeight: 500, fontSize: '.75rem', lineHeight: '1.125rem' } }, root: { @@ -40,110 +32,97 @@ const tableStyles = { position: 'relative', borderBottom: `1px solid ${gray100}`, marginTop: '.25rem', - '& .MuiLink-root': { - color: 'red', - gap: '0.5rem', - fontSize: '0.875rem', - lineHeight: '1.25rem', - fontWeight: 600, - textDecoration: 'none' + color: 'red', gap: '0.5rem', fontSize: '0.875rem', lineHeight: '1.25rem', fontWeight: 600, textDecoration: 'none' }, '& .MuiIconButton-root': { - padding: '0', - backgroundColor: 'transparent', - '& .MuiSvgIcon-root': { - fontSize: '1rem', - color: gray500, - }, + padding: '0', backgroundColor: 'transparent', + '& .MuiSvgIcon-root': { fontSize: '1rem', color: gray500 }, }, '& .MuiTypography-root': { - color: gray700, - fontSize: '0.875rem', - fontWeight: 400, - lineHeight: '1.25rem', - overflow: "hidden", - textOverflow: "ellipsis", - whiteSpace: "nowrap" + color: gray700, fontSize: '0.875rem', fontWeight: 400, lineHeight: '1.25rem', + overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, '& > .MuiBox-root': { - display: 'flex', - alignItems: 'center', - minWidth: 0, - gap: '0.5rem', - paddingRight: '0.75rem', - paddingLeft: 0 + display: 'flex', alignItems: 'center', minWidth: 0, gap: '0.5rem', paddingRight: '0.75rem', paddingLeft: 0 }, '&:not(.secondary)': { '&:hover': { - background: gray50, - borderColor: gray100, - borderRadius: '0.5rem', - + background: gray50, borderColor: gray100, borderRadius: '0.5rem', '&:before': { - content: '""', - height: '1.5rem', - width: '0.125rem', - background: brand600, - position: 'absolute', - left: '0rem', - top: '50%', - transform: 'translateY(-50%)', - margin: 'auto 0', - borderRadius: '0.1875rem' + content: '""', height: '1.5rem', width: '0.125rem', background: brand600, + position: 'absolute', left: '0rem', top: '50%', transform: 'translateY(-50%)', + margin: 'auto 0', borderRadius: '0.1875rem' }, } }, }, inputParentBox: { - width: '100%', - borderRadius: '0.5rem', - background: '#F0F2F2', + width: '100%', borderRadius: '0.5rem', background: '#F0F2F2', '&:before': { - content: '""', - height: '1.5rem', - width: '0.125rem', - background: brand600, - position: 'absolute', - left: '0rem', - top: '50%', - transform: 'translateY(-50%)', - margin: 'auto 0', - borderRadius: '0.1875rem' + content: '""', height: '1.5rem', width: '0.125rem', background: brand600, + position: 'absolute', left: '0rem', top: '50%', transform: 'translateY(-50%)', + margin: 'auto 0', borderRadius: '0.1875rem' } }, input: { '& .MuiOutlinedInput-root': { - borderRadius: '0.5rem', - fontSize: '0.875rem', - color: '#313534', - background: '#fff', - boxShadow: '0px 1px 2px 0px rgba(16, 24, 40, 0.05)' + borderRadius: '0.5rem', fontSize: '0.875rem', color: '#313534', + background: '#fff', boxShadow: '0px 1px 2px 0px rgba(16, 24, 40, 0.05)' }, - '& input': { - padding: '0.5rem 0.75rem', - height: '2.25rem' - }, - '& .Mui-focused': { - border: '2px solid #1C5F54', - background: '#F0F2F2', - color: '#313534' - } + '& input': { padding: '0.5rem 0.75rem', height: '2.25rem' }, + '& .Mui-focused': { border: '2px solid #1C5F54', background: '#F0F2F2', color: '#313534' } }, confirmButton: { - p: '0.5rem 0.75rem', - background: 'transparent', - color: brand700, - '&:hover': { - background: brand50, - color: brand700 - } + p: '0.5rem 0.75rem', background: 'transparent', color: brand700, + '&:hover': { background: brand50, color: brand700 } } }; +// ---------- helpers ---------- +const safe = (v) => (v == null ? "" : String(v)); + +function normalizeTableData(data) { + if (!data) return []; + if (Array.isArray(data.tableData) && data.tableData.length) { + return data.tableData.map((r, i) => ({ + id: r.id ?? `${safe(r.subject)}|${safe(r.predicate)}|${safe(r.object)}|${i}`, + subject: safe(r.subject), + predicate: safe(r.predicate), + object: safe(r.object), + })); + } + const rows = Array.isArray(data.rows) && data.rows.length + ? data.rows + : (Array.isArray(data.values) ? data.values : []); + if (rows && rows.length) { + return rows.map((r, i) => ({ + id: `${safe(r.subjectId || r.subject)}|${safe(data.title)}|${safe(r.objectId || r.object)}|${i}`, + subject: safe(r.subject || r.subjectId), + predicate: safe(data.title || "predicate"), + object: safe(r.object || r.objectId), + })); + } + if (Array.isArray(data.edges) && data.edges.length) { + return data.edges.map((e, i) => { + const subj = e?.from?.label || e?.from?.id; + const obj = e?.to?.label || e?.to?.id; + const pred = e?.predicate?.label || e?.predicate?.id || data.title || "predicate"; + return { + id: `${safe(subj)}|${safe(pred)}|${safe(obj)}|${i}`, + subject: safe(subj), + predicate: safe(pred), + object: safe(obj), + }; + }); + } + return []; +} + +// ---------- component ---------- const CustomizedTable = ({ data, term, isAddButtonVisible }) => { const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); - const [tableContent, setTableContent] = useState(data?.tableData); + const [tableContent, setTableContent] = useState(() => normalizeTableData(data)); const [tableHeader, setTableHeader] = useState([ { key: 'subject', label: 'Subject', allowSort: false, direction: 'desc' }, { key: 'predicate', label: 'Predicates', allowSort: false }, @@ -162,32 +141,27 @@ const CustomizedTable = ({ data, term, isAddButtonVisible }) => { const targetRow = useRef(); const sourceRow = useRef(); - const move = (arr, fromIndex, toIndex) => { - let element = arr[fromIndex]; - arr.splice(fromIndex, 1); - arr.splice(toIndex, 0, element); - return arr; - }; - const dragStart = (id, index) => { - sourceRow.current = { id, index }; - }; + // keep table in sync with prop `data` + useEffect(() => { + setTableContent(normalizeTableData(data)); + }, [data]); - const dragEnter = (id, index) => { - targetRow.current = { id, index }; + const move = (arr, fromIndex, toIndex) => { + const element = arr[fromIndex]; + const copy = [...arr]; + copy.splice(fromIndex, 1); + copy.splice(toIndex, 0, element); + return copy; }; + const dragStart = (id, index) => { sourceRow.current = { id, index }; }; + const dragEnter = (id, index) => { targetRow.current = { id, index }; }; const onReorder = (source, target) => { - if (source.index === target.index) { - return; - } - const updatedContent = move([...tableContent], source.index, target.index); - setTableContent(updatedContent); - }; - - const dragEnd = () => { - onReorder(sourceRow.current, targetRow.current); + if (!source || !target || source.index === target.index) return; + setTableContent((prev) => move(prev, source.index, target.index)); }; + const dragEnd = () => onReorder(sourceRow.current, targetRow.current); const requestSort = (e, key) => { if (!key) return; @@ -195,67 +169,53 @@ const CustomizedTable = ({ data, term, isAddButtonVisible }) => { e.preventDefault(); let direction = 'asc'; - if (sortConfig.key === key && sortConfig.direction === 'asc') { - direction = 'desc'; - } else if (sortConfig.key === key && sortConfig.direction === 'desc') { - direction = 'asc'; - } + if (sortConfig.key === key && sortConfig.direction === 'asc') direction = 'desc'; + else if (sortConfig.key === key && sortConfig.direction === 'desc') direction = 'asc'; + setSortConfig({ key, direction }); - const updatedHeader = tableHeader.map((item) => { - if (item.key === key) { - return { ...item, direction }; - } - return item; - }); - setTableHeader(updatedHeader); + setTableHeader((prev) => + prev.map((item) => (item.key === key ? { ...item, direction } : item)) + ); setTableContent((prevContent) => { - const sortedContent = [...prevContent]; - sortedContent.sort((a, b) => { - if (a[key] < b[key]) return direction === 'asc' ? -1 : 1; - if (a[key] > b[key]) return direction === 'asc' ? 1 : -1; - return 0; + const sorted = [...prevContent].sort((a, b) => { + const av = safe(a[key]).toLowerCase(); + const bv = safe(b[key]).toLowerCase(); + const cmp = av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' }); + return direction === 'asc' ? cmp : -cmp; }); - return sortedContent; + return sorted; }); }; const getSortIcon = (key) => { const column = tableHeader.find((item) => item.key === key); - if (column && column.direction) { - return column.direction === 'asc' ? : ; + if (column?.direction) { + return column.direction === 'asc' + ? + : ; } return ; }; - const handleOpenEditTermDialog = () => { - setEditTermDialogOpen(true); - }; - - const handleCloseEditTermDialog = () => { - setEditTermDialogOpen(false); - }; - - const handleUndoDelete = () => { - console.log("Undo deletion!") - }; - + const handleOpenEditTermDialog = () => setEditTermDialogOpen(true); + const handleCloseEditTermDialog = () => setEditTermDialogOpen(false); + const handleUndoDelete = () => {}; const handleSnackbarClose = (event, reason) => { if (reason === "clickaway") return; setSnackbarOpen(false); }; + // NOTE: rename inner var to avoid shadowing prop `data` (fixes react/prop-types lint) // eslint-disable-next-line react-hooks/exhaustive-deps const fetchTerms = useCallback(debounce(async (searchTerm) => { - const data = await getMatchTerms(user?.groupname, searchTerm); - setTerms(data?.results[0]); - }, 500), [getMatchTerms]); + const resp = await getMatchTerms(user?.groupname, searchTerm); + setTerms(resp?.results?.[0]); + }, 500), [user?.groupname]); useEffect(() => { - if (objectSearchTerm) { - fetchTerms(objectSearchTerm); - } + if (objectSearchTerm) fetchTerms(objectSearchTerm); }, [objectSearchTerm, fetchTerms]); const tableWidth = 800; @@ -272,19 +232,17 @@ const CustomizedTable = ({ data, term, isAddButtonVisible }) => { requestSort(e, head.key)} - sx={{ - transition: 'opacity 0.3s', - marginLeft: '0.5rem' - }} + sx={{ transition: 'opacity 0.3s', marginLeft: '0.5rem' }} > {getSortIcon(head.key)} )} ))} - + - {tableContent.map((row, index) => + + {(tableContent || []).map((row, index) => ( { onDragEnter={dragEnter} onDragEnd={dragEnd} /> - )} + ))} + {isAddButtonVisible && ( @@ -306,14 +265,32 @@ const CustomizedTable = ({ data, term, isAddButtonVisible }) => { )} - - + + + ); }; CustomizedTable.propTypes = { - data: PropTypes.object, + data: PropTypes.shape({ + title: PropTypes.string, + count: PropTypes.number, + tableData: PropTypes.array, // legacy + rows: PropTypes.array, // new + values: PropTypes.array, // new alias + edges: PropTypes.array // fallback + }), term: PropTypes.string, isAddButtonVisible: PropTypes.bool }; diff --git a/src/components/SingleTermView/OverView/Hierarchy.jsx b/src/components/SingleTermView/OverView/Hierarchy.jsx index e1cf61da..7b85a47b 100644 --- a/src/components/SingleTermView/OverView/Hierarchy.jsx +++ b/src/components/SingleTermView/OverView/Hierarchy.jsx @@ -7,55 +7,155 @@ import { } from "@mui/material"; import { vars } from "../../../theme/variables"; import React from "react"; -import { RestartAlt, TargetCross} from "../../../Icons"; +import PropTypes from "prop-types"; +import { RestartAlt, TargetCross } from "../../../Icons"; import SingleSearch from "../SingleSearch"; import CustomizedTreeView from "../../common/CustomizedTreeView"; import CustomSingleSelect from "../../common/CustomSingleSelect"; const { gray600, gray800 } = vars; -const options = [ - { label: 'Oliver Hansen', handler: 'Oliver'}, - { label: 'April Tucker', handler: 'April'}, - { label: 'Van Henry', handler: 'Van'}, - { label: 'Omar Alexander', handler: 'Omar'} -]; -const Hierarchy = () => { - const [type, setType] = React.useState('children'); - const [selectedValue, setSelectedValue] = React.useState(null); - - const handleSelectChange = (value) => { - setSelectedValue(value); + +function mapsFromTriples(triples) { + const idLabelMap = {}; + const parentToChildren = {}; + + for (const t of triples || []) { + const pred = (t.predicate?.label || t.predicate?.id || "").toLowerCase(); + + if (pred.endsWith("label") || pred.includes("rdfs:label")) { + if (t.subject?.id) idLabelMap[t.subject.id] = t.object?.label || t.object?.id || t.subject.id; + continue; + } + + if (pred.endsWith("ilx.partof:") || pred.includes("partof") || pred.includes("is part of")) { + const child = t.subject?.id; + const parent = t.object?.id; + if (child && parent) { + if (!parentToChildren[parent]) parentToChildren[parent] = []; + if (!parentToChildren[parent].includes(child)) parentToChildren[parent].push(child); + } + } } + return { idLabelMap, parentToChildren }; +} + +function buildTree(rootId, mapping, idLabelMap) { + const visited = new Set(); + const build = (id) => { + if (visited.has(id)) return { id, label: idLabelMap[id] || id, children: [], isCycle: true }; + visited.add(id); + const kids = (mapping[id] || []).map(build); + return { id, label: idLabelMap[id] || id, children: kids }; + }; + return [build(rootId)]; +} + +const Hierarchy = ({ + options = [], + selectedValue, + onSelect, + triplesChildren = [], + triplesSuperclasses = [], +}) => { + const [type, setType] = React.useState("children"); + const [treeData, setTreeData] = React.useState([]); + const [loading, setLoading] = React.useState(false); + + React.useEffect(() => { + const triples = type === "children" ? triplesChildren : triplesSuperclasses; + if (!selectedValue?.handler || !Array.isArray(triples)) { + setTreeData([]); + return; + } + setLoading(true); + try { + const { idLabelMap, parentToChildren } = mapsFromTriples(triples); + let mapping = parentToChildren; + if (type === "superclasses") { + const childToParents = {}; + for (const [parent, kids] of Object.entries(parentToChildren)) { + for (const kid of kids) { + if (!childToParents[kid]) childToParents[kid] = []; + if (!childToParents[kid].includes(parent)) childToParents[kid].push(parent); + } + } + mapping = childToParents; + } + const tree = buildTree(selectedValue.handler, mapping, idLabelMap); + setTreeData(tree); + } catch (e) { + console.error(e); + setTreeData([]); + } finally { + setLoading(false); + } + }, [selectedValue, type, triplesChildren, triplesSuperclasses]); + + const childCount = treeData?.[0]?.children?.length || 0; + return ( - - + + Hierarchy - - Type: - setType(v)} options={[ - { - value: 'children', - label: 'Children', - }, - { - value: 'superclasses', - label: 'Superclasses', - }, - ]} /> + + + Type: + + setType(v)} + options={[ + { value: "children", label: "Children" }, + { value: "superclasses", label: "Superclasses" }, + ]} + /> - - - - - Total number of first generation children: 3 + + + + + Total number of first generation {type === "children" ? "children" : "superclasses"}: {childCount} + - )} + ); +}; + +Hierarchy.propTypes = { + options: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string, + handler: PropTypes.string.isRequired, + }) + ), + selectedValue: PropTypes.shape({ + label: PropTypes.string, + handler: PropTypes.string, + }), + onSelect: PropTypes.func, + triplesChildren: PropTypes.array, + triplesSuperclasses: PropTypes.array, +}; -export default Hierarchy \ No newline at end of file +export default Hierarchy; diff --git a/src/components/SingleTermView/OverView/OverView.jsx b/src/components/SingleTermView/OverView/OverView.jsx index 41d4078d..c35bdc77 100644 --- a/src/components/SingleTermView/OverView/OverView.jsx +++ b/src/components/SingleTermView/OverView/OverView.jsx @@ -10,33 +10,93 @@ import Hierarchy from "./Hierarchy"; import Predicates from "./Predicates"; import RawDataViewer from "./RawDataViewer"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { getMatchTerms, getRawData } from "../../../api/endpoints/apiService"; +import { getMatchTerms, getRawData, getTermHierarchies } from "../../../api/endpoints/apiService"; const OverView = ({ searchTerm, isCodeViewVisible, selectedDataFormat, group = "base" }) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [jsonData, setJsonData] = useState(null); + // options for Hierarchy’s SingleSearch + const [hierarchyOptions, setHierarchyOptions] = useState([]); + const [selectedValue, setSelectedValue] = useState(null); + + // hierarchies for the currently selected value + const [triplesChildren, setTriplesChildren] = useState([]); + const [triplesSuperclasses, setTriplesSuperclasses] = useState([]); + // eslint-disable-next-line react-hooks/exhaustive-deps const fetchTerms = useCallback( - debounce((searchTerm) => { - if (searchTerm) { - getMatchTerms(group, searchTerm).then(data => { - console.log("data from api call: ", data) - setData(data?.results?.[0]); + debounce((term) => { + if (term) { + getMatchTerms(group, term).then(apiData => { + const results = apiData?.results || []; + setData(results?.[0] || null); + + // Build options { label, handler } + const opts = results + .map((r) => { + const label = + r.label || + r.rdfsLabel || + r.prefLabel || + r.term || + r.name || + r.curie || + r.id; + const handler = + r.curie || r.ilx || r.id || r.termId || r.identifier; + return label && handler ? { label, handler } : null; + }) + .filter(Boolean); + + // de-dupe by handler + const seen = new Set(); + const deduped = opts.filter(o => (seen.has(o.handler) ? false : (seen.add(o.handler), true))); + + setHierarchyOptions(deduped); + + // default selection + setSelectedValue(prev => + prev && deduped.some(o => o.handler === prev?.handler) ? prev : deduped[0] || null + ); + setLoading(false); }); + } else { + setData(null); + setHierarchyOptions([]); + setSelectedValue(null); + setLoading(false); } }, 300), [group] ); const fetchJSONFile = useCallback(() => { + if (!searchTerm) { + setJsonData(null); + return; + } getRawData(group, searchTerm, 'jsonld').then(rawResponse => { setJsonData(rawResponse); - }) + }); }, [searchTerm, group]); + // Fetch hierarchies for selectedValue + const fetchHierarchies = useCallback(async (curieLike, groupname) => { + try { + await Promise.all([ + getTermHierarchies({ groupname, termId: curieLike, objToSub: true }), + getTermHierarchies({ groupname, termId: curieLike, objToSub: false }), + ]); + } catch (e) { + console.error("fetchHierarchies error:", e); + setTriplesChildren([]); + setTriplesSuperclasses([]); + } + }, []); + useEffect(() => { setLoading(true); fetchTerms(searchTerm); @@ -46,30 +106,52 @@ const OverView = ({ searchTerm, isCodeViewVisible, selectedDataFormat, group = " }; }, [searchTerm, fetchTerms, fetchJSONFile]); + useEffect(() => { + // infer groupname from data if you store it in context; fallback to "base" + const groupname = group || "base"; + if (selectedValue?.handler) { + fetchHierarchies(selectedValue.handler, groupname); + } else { + setTriplesChildren([]); + setTriplesSuperclasses([]); + } + }, [selectedValue, group, fetchHierarchies]); + const memoData = useMemo(() => data, [data]); return ( - - {isCodeViewVisible ? : + + {isCodeViewVisible ? ( + + ) : ( <>
- + - + - } + )} - ) + ); } OverView.propTypes = { diff --git a/src/components/SingleTermView/OverView/Predicates.jsx b/src/components/SingleTermView/OverView/Predicates.jsx index 4f8720a6..9af95822 100644 --- a/src/components/SingleTermView/OverView/Predicates.jsx +++ b/src/components/SingleTermView/OverView/Predicates.jsx @@ -3,56 +3,123 @@ import PropTypes from 'prop-types'; import ExpandIcon from '@mui/icons-material/Expand'; import RemoveIcon from '@mui/icons-material/Remove'; import PredicatesAccordion from "./PredicatesAccordion"; -import {Box, Typography, ToggleButton, ToggleButtonGroup} from "@mui/material"; - +import { Box, Typography, ToggleButton, ToggleButtonGroup } from "@mui/material"; import { vars } from "../../../theme/variables"; + const { gray800 } = vars; -const Predicates = ({ data, isGraphVisible }) => { - const [predicates, setPredicates] = React.useState([]); - const [toggleButtonValue, setToggleButtonValue] = React.useState('expand') +const FALLBACK_PREDICATE = "predicate"; - const onToggleButtonChange = (event, newValue) => { - if (newValue) { - setToggleButtonValue(newValue) - } +function groupsFromTriples(triples) { + const byPred = new Map(); + for (const t of triples || []) { + const key = (t?.predicate?.label || t?.predicate?.id || "").trim() || FALLBACK_PREDICATE; + if (!byPred.has(key)) byPred.set(key, []); + byPred.get(key).push(t); + } + const groups = []; + for (const [predLabel, items] of byPred.entries()) { + const rows = items.map((t) => ({ + subject: t.subject?.label || t.subject?.id, + subjectId: t.subject?.id, + object: t.object?.label || t.object?.id, + objectId: t.object?.id, + })); + const edges = items.map((t) => ({ + from: t.subject, + to: t.object, + predicate: t.predicate, + })); + groups.push({ + title: predLabel, + count: items.length, + rows, + values: rows, + edges, + forceGraph: false, + }); } + return groups; +} + +const Predicates = ({ basePredicates = [], triplesChildren = [], triplesSuperclasses = [], isGraphVisible }) => { + const [predicates, setPredicates] = React.useState([]); + const [toggleButtonValue, setToggleButtonValue] = React.useState('expand'); + + const onToggleButtonChange = (_event, newValue) => { + if (newValue) setToggleButtonValue(newValue); + }; React.useEffect(() => { - data?.predicates && setPredicates(data?.predicates) - }, [data]); - - return - - Predicates - - - - - - - - - + // Build groups from both directions and merge with existing basePredicates + const groupsChildren = groupsFromTriples(triplesChildren); + const groupsSupers = groupsFromTriples(triplesSuperclasses); + + const all = [...(Array.isArray(basePredicates) ? basePredicates : [])]; + + const pushOrMerge = (g) => { + const idx = all.findIndex(p => p.title === g.title); + if (idx === -1) { + all.push(g); + } else { + const existing = all[idx]; + const combinedRows = [...(existing.rows || existing.values || []), ...(g.rows || g.values || [])]; + const combinedEdges = [...(existing.edges || []), ...(g.edges || [])]; + all[idx] = { + ...existing, + count: (existing.count || 0) + (g.count || 0), + rows: combinedRows, + values: combinedRows, + edges: combinedEdges, + }; + } + }; + + groupsChildren.forEach(pushOrMerge); + groupsSupers.forEach(pushOrMerge); + + setPredicates(all); + }, [basePredicates, triplesChildren, triplesSuperclasses]); + + return ( + + + Predicates + + + + + + + + + + + - - -} + ); +}; Predicates.propTypes = { - data: PropTypes.object, + basePredicates: PropTypes.array, + triplesChildren: PropTypes.array, + triplesSuperclasses: PropTypes.array, isGraphVisible: PropTypes.bool -} +}; export default Predicates; diff --git a/src/components/SingleTermView/OverView/PredicatesAccordion.jsx b/src/components/SingleTermView/OverView/PredicatesAccordion.jsx index 4a89081c..ed38e43b 100644 --- a/src/components/SingleTermView/OverView/PredicatesAccordion.jsx +++ b/src/components/SingleTermView/OverView/PredicatesAccordion.jsx @@ -26,14 +26,11 @@ const { gray600 } = vars; const PredicatesAccordion = ({ data, expandAllPredicates, isGraphVisible }) => { const [toggleButtonValues, setToggleButtonValues] = useState(data?.map(() => 'tableView') || []); const [openViewDiagram, setOpenViewDiagram] = React.useState(false); - const [selectedItem, setSelectedItem] = useState(null) + const [selectedItem, setSelectedItem] = useState(null); const [expandedItems, setExpandedItems] = useState(data?.map(() => false) || []); const query = useQuery(); const term = query.get('searchTerm'); - const imgStyle = { width: '100%' }; - const imgPath = '/success.png'; - const onToggleButtonChange = (index) => (event, newValue) => { if (newValue) { event.preventDefault(); @@ -43,30 +40,31 @@ const PredicatesAccordion = ({ data, expandAllPredicates, isGraphVisible }) => { setToggleButtonValues(newTabValues); } }; - const handleClickViewDiagram = (e, item) => { - setSelectedItem(item) + + const handleClickViewDiagram = (_e, item) => { + setSelectedItem(item); setOpenViewDiagram(true); }; - const handleCloseViewDiagram = () => { - setOpenViewDiagram(false); - }; + const handleCloseViewDiagram = () => setOpenViewDiagram(false); - const handleAccordionChange = (index) => (event, isExpanded) => { + const handleAccordionChange = (index) => (_event, isExpanded) => { const newExpandedItems = [...expandedItems]; newExpandedItems[index] = isExpanded; setExpandedItems(newExpandedItems); }; useEffect(() => { - const newToggleButtonValues = data?.map(() => "tableView") || []; + const newToggleButtonValues = data?.map((d) => (d.forceGraph ? "graphView" : "tableView")) || []; const newExpandedItems = data?.map(() => expandAllPredicates) || []; - setToggleButtonValues(newToggleButtonValues); setExpandedItems(newExpandedItems); }, [data, expandAllPredicates]); + // lightweight image for the dialog (existing behavior) + const imgStyle = { width: '100%' }; + const imgPath = '/success.png'; const image = new Image(); - image.onload = () => preview + image.onload = () => preview; image.src = imgPath; return ( @@ -79,11 +77,7 @@ const PredicatesAccordion = ({ data, expandAllPredicates, isGraphVisible }) => { expanded={expandedItems[index] ?? false} onChange={handleAccordionChange(index)} square - sx={{ - "&.MuiPaper-root": { - backgroundColor: "transparent" - } - }} + sx={{ "&.MuiPaper-root": { backgroundColor: "transparent" } }} > } @@ -91,9 +85,7 @@ const PredicatesAccordion = ({ data, expandAllPredicates, isGraphVisible }) => { id={`panel${index + 1}-header`} > - - {pred.title} - + {pred.title} @@ -116,12 +108,16 @@ const PredicatesAccordion = ({ data, expandAllPredicates, isGraphVisible }) => { - ) : <>} + ) : null} {toggleButtonValues[index] === 'tableView' ? ( - + ) : ( @@ -129,10 +125,8 @@ const PredicatesAccordion = ({ data, expandAllPredicates, isGraphVisible }) => { variant='outlined' onClick={(e) => handleClickViewDiagram(e, pred)} disableRipple - sx={{ - minWidth: 'auto', - alignSelf: 'flex-end' - }}> + sx={{ minWidth: 'auto', alignSelf: 'flex-end' }} + > @@ -140,16 +134,15 @@ const PredicatesAccordion = ({ data, expandAllPredicates, isGraphVisible }) => { ))} - { - openViewDiagram && - } - + )} ); }; diff --git a/src/components/common/CustomizedTreeView.jsx b/src/components/common/CustomizedTreeView.jsx index cb692b54..0648b07f 100644 --- a/src/components/common/CustomizedTreeView.jsx +++ b/src/components/common/CustomizedTreeView.jsx @@ -1,113 +1,77 @@ -import {Box, Chip} from "@mui/material"; +import PropTypes from 'prop-types'; +import { Box, Chip, CircularProgress } from "@mui/material"; import { styled } from '@mui/material/styles'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; import { TreeItem, treeItemClasses } from '@mui/x-tree-view/TreeItem'; import ExpandLessOutlinedIcon from '@mui/icons-material/ExpandLessOutlined'; import ChevronRightOutlinedIcon from '@mui/icons-material/ChevronRightOutlined'; +import { vars } from "../../theme/variables"; -import {vars} from "../../theme/variables"; -const {gray500, brand200, brand50} = vars +const { gray500, brand200, brand50 } = vars; -const ITEMS = [ - { - id: '1', - label: 'Central nervous system', - children: [ - { id: '2', label: 'Hello' }, - { - id: '3', - label: 'Subtree with children', - children: [ - { id: '6', label: 'Hello' }, - { - id: '7', - label: 'Sub-subtree with children', - children: [ - { id: '9', label: 'Child 1' }, - { id: '10', label: 'Child 2' }, - { id: '11', label: 'Child 3' }, - ], - }, - { id: '8', label: 'Hello' }, - ], - }, - { id: '4', label: 'World' }, - { id: '5', label: 'Something something' }, - ], - }, - { - id: '12', - label: 'Main', - children: [ - { id: '22', label: 'Hello' }, - { - id: '32', - label: 'Subtree with children', - children: [ - { id: '62', label: 'Hello' }, - { - id: '72', - label: 'Sub-subtree with children', - children: [ - { id: '92', label: 'Child 1' }, - { id: '102', label: 'Child 2' }, - { id: '112', label: 'Child 3' }, - ], - }, - { id: '82', label: 'Hello' }, - ], - }, - { id: '42', label: 'World' }, - { id: '52', label: 'Something something' }, - ], - }, -]; - -const StyledTreeItem = styled((props) => ( +const StyledTreeItemBase = (props) => ( - {props.label} - { - props.itemId === '1' && - } - } + label={ + + {props.label} + {props.currentId && props.itemId === props.currentId && ( + + )} + + } /> -))(() => ({ - color: gray500, +); + +StyledTreeItemBase.propTypes = { + label: PropTypes.node, + currentId: PropTypes.string, + itemId: PropTypes.string, // provided by MUI TreeItem +}; +const StyledTreeItem = styled(StyledTreeItemBase)(() => ({ + color: gray500, [`& .${treeItemClasses.content}`]: { - [`& .${treeItemClasses.label}`]: { - fontSize: '0.875rem', - fontWeight: 400, - }, - }, - [`& .${treeItemClasses.groupTransition}`]: { - marginLeft: 14, + [`& .${treeItemClasses.label}`]: { fontSize: '0.875rem', fontWeight: 400 }, }, + [`& .${treeItemClasses.groupTransition}`]: { marginLeft: 14 }, })); -const CustomizedTreeView = () => { +const CustomizedTreeView = ({ items = [], loading = false, currentId = null }) => { + if (loading) { + return ( + + + + ); + } + return ( ); -} +}; + +CustomizedTreeView.propTypes = { + items: PropTypes.array, + loading: PropTypes.bool, + currentId: PropTypes.string, +}; -export default CustomizedTreeView +export default CustomizedTreeView; diff --git a/vite.config.js b/vite.config.js index d8ee02cf..b3b39372 100644 --- a/vite.config.js +++ b/vite.config.js @@ -159,7 +159,27 @@ export default defineConfig({ res.setHeader('Access-Control-Allow-Credentials', 'true'); }); }, - } + }, + '^/[^/]+/query/transitive/.*': { + target: 'https://uri.olympiangods.org', + changeOrigin: true, + secure: false, + rewrite: (path) => path, // keep full path + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq, req) => { + // pass through auth/cookies if present + if (req.headers.authorization) proxyReq.setHeader('Authorization', req.headers.authorization); + if (req.headers.cookie) proxyReq.setHeader('Cookie', req.headers.cookie); + }); + proxy.on('proxyRes', (proxyRes, req, res) => { + // helpful CORS for credentialed requests in dev + const origin = req.headers.origin; + if (origin) res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Expose-Headers', 'X-Redirect-Location'); + }); + }, + }, }, }, });