diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js
index 06a045bb..d54400c3 100644
--- a/src/components/GraphViewer/GraphViewer.js
+++ b/src/components/GraphViewer/GraphViewer.js
@@ -327,7 +327,16 @@ const GraphViewer = (props) => {
linkCanvasObjectMode={'replace'}
onLinkHover={handleLinkHover}
// Override drawing of canvas objects, draw an image as a node
- nodeCanvasObject={(node, ctx) => paintNode(node, ctx, hoverNode, selectedNode, nodeSelected, previouslySelectedNodes)}
+ nodeCanvasObject={(node, ctx) =>
+ paintNode(
+ node,
+ ctx,
+ hoverNode,
+ selectedNode,
+ nodeSelected,
+ previouslySelectedNodes,
+ selectedLayout.layout === LEFT_RIGHT.layout
+ )}
nodeCanvasObjectMode={node => 'replace'}
nodeVal = { node => {
if ( selectedLayout.layout === TOP_DOWN.layout ){
diff --git a/src/components/NodeDetailView/Details/SubjectDetails.js b/src/components/NodeDetailView/Details/SubjectDetails.js
index 652ce87c..1b78d4ae 100644
--- a/src/components/NodeDetailView/Details/SubjectDetails.js
+++ b/src/components/NodeDetailView/Details/SubjectDetails.js
@@ -41,7 +41,7 @@ const SubjectDetails = (props) => {
if ( property.isGroup ){
return (
{property.label}
-
+
)
}
diff --git a/src/components/NodeDetailView/Details/Views/SimpleChip.js b/src/components/NodeDetailView/Details/Views/SimpleChip.js
index 2db276fc..31e879da 100644
--- a/src/components/NodeDetailView/Details/Views/SimpleChip.js
+++ b/src/components/NodeDetailView/Details/Views/SimpleChip.js
@@ -1,14 +1,78 @@
-import React from "react";
-import {
- Box,
- Chip
-} from "@material-ui/core";
+import React, { useState } from "react";
+import { Box, Chip, Menu, MenuItem } from "@material-ui/core";
+
+const SimpleChip = ({ chips }) => {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const [selectedItem, setSelectedItem] = useState(null);
+
+ const isUrl = (value) => {
+ if (!value) return false;
+ try {
+ new URL(value);
+ return true;
+ } catch {
+ return false;
+ }
+ };
+
+ const normalize = (item) =>
+ typeof item === "string" ? { value: item } : item;
+
+ const handleClick = (item) => {
+ const url = item?.link || (isUrl(item?.value) ? item?.value : null);
+ if (url) {
+ window.open(url, "_blank");
+ }
+ };
+
+ const handleContextMenu = (event, item) => {
+ event.preventDefault();
+ setAnchorEl(event.currentTarget);
+ setSelectedItem(item);
+ };
+
+ const handleMenuClose = () => {
+ setAnchorEl(null);
+ setSelectedItem(null);
+ };
+
+ const handleCopyId = () => {
+ const copyValue = selectedItem?.id || selectedItem?.link || selectedItem?.value;
+ if (copyValue) {
+ navigator.clipboard.writeText(copyValue);
+ }
+ handleMenuClose();
+ };
+
+ const handleOpenNewTab = () => {
+ const url = selectedItem?.link || selectedItem?.id || selectedItem?.value;
+ if (isUrl(url)) {
+ window.open(url, "_blank");
+ }
+ handleMenuClose();
+ };
-const SimpleChip = ({chips}) => {
return (
- { chips?.map((item, index) => ) }
+ {chips?.map((rawItem, index) => {
+ const item = normalize(rawItem);
+ return (
+ handleClick(item)}
+ onContextMenu={(e) => handleContextMenu(e, item)}
+ />
+ );
+ })}
+
- )
-}
+ );
+};
+
export default SimpleChip;
diff --git a/src/components/NodeDetailView/Details/Views/SimpleLinkedChip.js b/src/components/NodeDetailView/Details/Views/SimpleLinkedChip.js
index fd6b7a8c..ce6e6aae 100644
--- a/src/components/NodeDetailView/Details/Views/SimpleLinkedChip.js
+++ b/src/components/NodeDetailView/Details/Views/SimpleLinkedChip.js
@@ -1,45 +1,108 @@
-import React from "react";
+import React, { useState } from "react";
import {
Box,
- Chip
+ Chip,
+ Menu,
+ MenuItem
} from "@material-ui/core";
import { useDispatch } from 'react-redux';
import { selectGroup } from '../../../../redux/actions';
import { GRAPH_SOURCE } from '../../../../constants';
-const SimpleChip = ({chips, node}) => {
+const SimpleChip = ({ chips, node }) => {
const dispatch = useDispatch();
+ const [anchorEl, setAnchorEl] = useState(null);
+ const [selectedItem, setSelectedItem] = useState(null);
+
+ const isUrl = (value) => {
+ if (!value) return false;
+ try {
+ new URL(value);
+ return true;
+ } catch {
+ return false;
+ }
+ };
+
const handleClick = (item, node) => {
- if ( item.link ){
- window.open(item.link, '_blank')
- } else if ( item.value ) {
- let urlCheck = new RegExp("([a-zA-Z0-9]+://)?([a-zA-Z0-9_]+:[a-zA-Z0-9_]+@)?([a-zA-Z0-9.-]+\\.[A-Za-z]{2,4})(:[0-9]+)?(/.*)?");
- if(urlCheck.test(item.value)) {
- window.open(item.value, '_blank')
- } else {
- if ( node ) {
- dispatch(selectGroup({
+ if (item.link) {
+ window.open(item.link, '_blank');
+ } else if (item.value) {
+ if (isUrl(item.value)) {
+ window.open(item.value, '_blank');
+ } else if (node) {
+ dispatch(
+ selectGroup({
dataset_id: node.dataset_id,
graph_node: node?.id,
tree_node: node?.tree_reference?.id,
- source: GRAPH_SOURCE
- }));
- }
+ source: GRAPH_SOURCE,
+ })
+ );
}
}
- }
+ };
+
+ const handleContextMenu = (event, item) => {
+ event.preventDefault();
+ setAnchorEl(event.currentTarget);
+ setSelectedItem(item);
+ };
+
+ const handleMenuClose = () => {
+ setAnchorEl(null);
+ setSelectedItem(null);
+ };
+
+ const handleCopyId = () => {
+ const copyValue = selectedItem?.id || selectedItem?.link || selectedItem?.value;
+ if (copyValue) {
+ navigator.clipboard.writeText(copyValue);
+ }
+ handleMenuClose();
+ };
+
+ const handleOpenNewTab = () => {
+ const url = selectedItem?.link || selectedItem?.id || selectedItem?.value;
+ if (url) {
+ window.open(url, '_blank');
+ }
+ handleMenuClose();
+ };
return (
- { chips?.map((item, index) => ( node === undefined
- ? item.link ?
- ( handleClick(item, null)}/>)
- : ()
- : ( handleClick(item, node)}/>)))
- }
+ {chips?.map((item, index) =>
+ node === undefined ? (
+ item.link ? (
+ handleClick(item, null)}
+ onContextMenu={(e) => handleContextMenu(e, item)}
+ />
+ ) : (
+ handleContextMenu(e, item)}
+ />
+ )
+ ) : (
+ handleClick(item, node)}
+ onContextMenu={(e) => handleContextMenu(e, item)}
+ />
+ )
+ )}
+
- )
-}
+ );
+};
+
export default SimpleChip;
diff --git a/src/components/Sidebar/Sidebar.js b/src/components/Sidebar/Sidebar.js
index 9d9fcfb4..4f972d57 100644
--- a/src/components/Sidebar/Sidebar.js
+++ b/src/components/Sidebar/Sidebar.js
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useRef, useEffect } from 'react';
import { Box } from '@material-ui/core';
import SidebarHeader from './Header';
import SidebarContent from './List';
@@ -7,9 +7,39 @@ import SidebarFooter from './Footer';
const Sidebar = (props) => {
const [expand, setExpand] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
+ const [width, setWidth] = useState(300);
+ const [isResizing, setIsResizing] = useState(false);
+ const sidebarRef = useRef(null);
+
+ const startResizing = (e) => {
+ setIsResizing(true);
+ e.preventDefault();
+ };
+
+ useEffect(() => {
+ const handleMouseMove = (e) => {
+ if (!isResizing || !expand) return;
+ const sidebarLeft = sidebarRef.current?.getBoundingClientRect().left || 0;
+ const newWidth = e.clientX - sidebarLeft;
+ if (newWidth > 200) {
+ setWidth(newWidth);
+ }
+ };
+ const stopResizing = () => setIsResizing(false);
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', stopResizing);
+ return () => {
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', stopResizing);
+ };
+ }, [isResizing, expand]);
return (
-
+
{
expand={expand}
setOpenDatasetsListDialog={props.setOpenDatasetsListDialog}
/>
+ {expand && }
);
};
diff --git a/src/theme.js b/src/theme.js
index 03a26d75..51a632b7 100644
--- a/src/theme.js
+++ b/src/theme.js
@@ -490,6 +490,7 @@ const theme = createTheme({
display: 'flex',
flexDirection: 'column',
transition: primaryTransition,
+ position: 'relative',
'&.shrink': {
width: '4.125rem',
transition: primaryTransition,
@@ -664,14 +665,14 @@ const theme = createTheme({
marginRight: '0.625rem',
flexShrink: 0,
},
- '& .labelText': {
+ '& .labelText': {
fontWeight: 'normal',
flexGrow: 1,
fontSize: '0.8125rem',
lineHeight: '1rem',
color: whiteColor,
- },
- '& .MuiTreeItem-group': {
+ },
+ '& .MuiTreeItem-group': {
paddingLeft: '1.4375rem',
margin: 0,
},
@@ -830,7 +831,7 @@ const theme = createTheme({
},
},
- '& .no-instance': {
+ '& .no-instance': {
fontSize: '0.75rem',
display: 'flex',
alignItems: 'center',
@@ -842,6 +843,14 @@ const theme = createTheme({
textAlign: 'center',
},
},
+ '&-resizer': {
+ width: '0.3125rem',
+ cursor: 'col-resize',
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ bottom: 0,
+ },
'&-footer': {
boxShadow: `0 -4.75rem 3.0625rem -2.5625rem ${secondaryColor}`,
borderTop: `0.0625rem solid ${lightBorderColor}`,
diff --git a/src/utils/GraphViewerHelper.js b/src/utils/GraphViewerHelper.js
index 15b94569..a309ec8e 100644
--- a/src/utils/GraphViewerHelper.js
+++ b/src/utils/GraphViewerHelper.js
@@ -68,7 +68,15 @@ const roundRect = (ctx, x, y, width, height, radius, color, alpha) => {
ctx.fill();
};
-export const paintNode = (node, ctx, hoverNode, selectedNode, nodeSelected, previouslySelectedNodes) => {
+export const paintNode = (
+ node,
+ ctx,
+ hoverNode,
+ selectedNode,
+ nodeSelected,
+ previouslySelectedNodes,
+ showFullName = false
+) => {
const size = 7.5;
const nodeImageSize = [size * 2.4, size * 2.4];
const hoverRectDimensions = [size * 4.2, size * 4.2];
@@ -105,10 +113,11 @@ export const paintNode = (node, ctx, hoverNode, selectedNode, nodeSelected, prev
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
let nodeName = node.name;
- if (nodeName.length > 10) {
+ if (Array.isArray(nodeName)) {
+ nodeName = nodeName[0];
+ }
+ if (!showFullName && nodeName?.length > 10) {
nodeName = nodeName.substr(0, 9).concat('...');
- } else if ( Array.isArray(nodeName) ){
- nodeName = nodeName[0]?.substr(0, 9).concat('...');
}
const textProps = [nodeName, node.x, textHoverPosition[1]];
if (node === hoverNode || node?.id === selectedNode?.id || node?.id === nodeSelected?.id ) {
diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js
index 5b29c093..8a6f9ec2 100644
--- a/src/utils/Splinter.js
+++ b/src/utils/Splinter.js
@@ -455,14 +455,15 @@ class Splinter {
}
replaceNode(a) {
- let newNode = {"value": a};
- if( a?.includes(rdfTypes.NCBITaxon.key) || a?.includes(rdfTypes.PATO.key) || a?.includes(rdfTypes.UBERON.key) || a?.includes(rdfTypes.RRID.key) ) {
- let node = this.nodes.get(a);
+ let newNode = { "value": a };
+ if (typeof a === 'string') {
+ const node = this.nodes.get(a);
if (node) {
- newNode = {"value": node?.attributes.label[0], "link": node?.id};
+ newNode = { "value": node?.attributes.label[0], "link": node?.id };
+ } else if (a.startsWith('http')) {
+ newNode = { "value": a, "link": a };
}
}
-
return newNode;
}
@@ -501,22 +502,42 @@ class Splinter {
let updatedAbout = [];
dataset_node.level = 1;
let that = this;
- dataset_node?.attributes?.isAbout?.forEach( (a) => {
- updatedAbout.push(that.replaceNode(a));
+ dataset_node?.attributes?.isAbout?.forEach((a) => {
+ if (
+ typeof a === 'string' && (
+ a.startsWith('http') ||
+ a?.includes(rdfTypes.NCBITaxon.key) ||
+ a?.includes(rdfTypes.PATO.key) ||
+ a?.includes(rdfTypes.UBERON.key) ||
+ a?.includes(rdfTypes.RRID.key)
+ )
+ ) {
+ updatedAbout.push(that.replaceNode(a));
+ } else {
+ updatedAbout.push({ value: a });
+ }
});
dataset_node.attributes.isAbout = updatedAbout;
let updateTechniques = [];
- dataset_node.attributes.protocolEmploysTechnique?.forEach( (a) => {
- if( a.includes(rdfTypes.NCBITaxon.key) || a.includes(rdfTypes.PATO.key) || a.includes(rdfTypes.UBERON.key) ) {
+ dataset_node.attributes.protocolEmploysTechnique?.forEach((a) => {
+ if (
+ typeof a === 'string' && (
+ a.startsWith('http') ||
+ a?.includes(rdfTypes.NCBITaxon.key) ||
+ a?.includes(rdfTypes.PATO.key) ||
+ a?.includes(rdfTypes.UBERON.key) ||
+ a?.includes(rdfTypes.RRID.key)
+ )
+ ) {
let node = this.nodes.get(a);
if (node) {
- updateTechniques.push({"value": node?.attributes.label[0], "link": node?.id});
+ updateTechniques.push({ value: node?.attributes.label[0], link: node?.id });
} else {
- updateTechniques.push({"value": a});
+ updateTechniques.push({ value: a, link: a });
}
} else {
- updateTechniques.push({"value": a});
+ updateTechniques.push({ value: a });
}
});
dataset_node.attributes.protocolEmploysTechnique = updateTechniques;
@@ -539,34 +560,37 @@ class Splinter {
organise_subjects(target_node, link, groups, k, type){
let parent = this.nodes.get(k);
let keys = Object.keys(config.groups.order);
- keys.forEach( key => {
+ keys.forEach(key => {
let group = config.groups.order[key];
- if ( target_node.attributes[key]?.[0] ) {
- let source = this.nodes.get(target_node.attributes[key]?.[0]);
- if ( source !== undefined ) {
- target_node.attributes[key][0] = source.attributes.label[0];
+ if (target_node.attributes[key]?.[0]) {
+ // attributes may already be objects from previous passes
+ const raw = target_node.attributes[key][0];
+ const originalValue = typeof raw === "object" ? raw.value : raw;
+
+ let source = this.nodes.get(originalValue);
+ let label = originalValue;
+ if (source !== undefined) {
+ label = source.attributes.label[0];
}
-
- const groupID = parent.id + "_" + target_node.attributes[key]?.[0].replace(/\s/g, "");
- if ( this.nodes.get(groupID) === undefined ) {
- let name = target_node.attributes[key]?.[0];
+ const groupID = parent.id + "_" + ("" + label).replace(/\s/g, "");
+ if (this.nodes.get(groupID) === undefined) {
const groupNode = {
id: groupID,
- name: name,
+ name: label,
type: typesModel.NamedIndividual.group.type,
properties: key,
- parent : parent,
+ parent: parent,
proxies: [],
level: parent.level + 1,
tree_reference: null,
children_counter: 0,
- collapsed : false,
- childLinks : [],
- samples : 0,
- subjects : 0,
- publishedURI : "",
- dataset_id : this.dataset_id
+ collapsed: false,
+ childLinks: [],
+ samples: 0,
+ subjects: 0,
+ publishedURI: "",
+ dataset_id: this.dataset_id
};
let nodeF = this.factory.createNode(groupNode);
const img = new Image();
@@ -577,11 +601,14 @@ class Splinter {
source: parent.id,
target: nodeF.id
});
- this.groups[key] ? this.groups[key][nodeF.name] = nodeF : this.groups[key] = {[nodeF.name] : nodeF};
+ this.groups[key] ? this.groups[key][nodeF.name] = nodeF : this.groups[key] = {[nodeF.name]: nodeF};
parent = groupNode;
} else {
parent = this.nodes.get(groupID);
}
+
+ // preserve original identifier for chips
+ target_node.attributes[key] = this.replaceNode(target_node.attributes[key][0]);
} else {
console.error("The group node already exists!", group.tag);
}
@@ -955,6 +982,7 @@ class Splinter {
}
}
});
+
}