Skip to content

Commit

Permalink
Support creating relationship on canvas by alt-clicking two nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
QubitPi committed Oct 23, 2023
1 parent 340f39d commit 3610689
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export function DetailsPane({
relType: vizItem.item.type
}}
graphStyle={graphStyle}
onGraphInteraction={onGraphInteraction}
sourceNodeId={vizItem.item.source.id}
targetNodeId={vizItem.item.target.id}
/>
)}
{vizItem.type === 'node' &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,44 +21,67 @@ import React from 'react'
import { Popup } from 'semantic-ui-react'

import { StyledRelationshipChip } from 'neo4j-arc/common'
import { GraphStyleModel } from 'neo4j-arc/graph-visualization'
import {
GraphInteractionCallBack,
GraphStyleModel,
REL_TYPE_UPDATE
} from 'neo4j-arc/graph-visualization'

import { GrassEditor } from './GrassEditor'

export type StyleableRelTypeProps = {
graphStyle: GraphStyleModel
selectedRelType: { relType: string; propertyKeys: string[]; count?: number }
onGraphInteraction?: GraphInteractionCallBack
sourceNodeId?: string
targetNodeId?: string
}
export function StyleableRelType({
selectedRelType,
graphStyle
graphStyle,
onGraphInteraction = () => undefined,
sourceNodeId,
targetNodeId
}: StyleableRelTypeProps): JSX.Element {
const styleForRelType = graphStyle.forRelationship({
type: selectedRelType.relType
})
return (
<Popup
on="click"
basic
key={selectedRelType.relType}
position="left center"
offset={[0, 0]}
trigger={
<StyledRelationshipChip
style={{
backgroundColor: styleForRelType.get('color'),
color: styleForRelType.get('text-color-internal')
}}
data-testid={`property-details-overview-relationship-type-${selectedRelType.relType}`}
>
{selectedRelType.count !== undefined
? `${selectedRelType.relType} (${selectedRelType.count})`
: `${selectedRelType.relType}`}
</StyledRelationshipChip>
<div
suppressContentEditableWarning={true}
contentEditable="true"
onInput={e =>
onGraphInteraction(REL_TYPE_UPDATE, {
sourceNodeId: sourceNodeId,
targetNodeId: targetNodeId,
oldType: selectedRelType.relType,
newType: e.currentTarget.textContent
})
}
wide
>
<GrassEditor selectedRelType={selectedRelType} />
</Popup>
<Popup
on="click"
basic
key={selectedRelType.relType}
position="left center"
offset={[0, 0]}
trigger={
<StyledRelationshipChip
style={{
backgroundColor: styleForRelType.get('color'),
color: styleForRelType.get('text-color-internal')
}}
data-testid={`property-details-overview-relationship-type-${selectedRelType.relType}`}
>
{selectedRelType.count !== undefined
? `${selectedRelType.relType} (${selectedRelType.count})`
: `${selectedRelType.relType}`}
</StyledRelationshipChip>
}
wide
>
<GrassEditor selectedRelType={selectedRelType} />
</Popup>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ import {
GraphInteractionCallBack,
GraphModel,
GraphVisualizer,
REL_ON_CANVAS_CREATE,
NODE_ON_CANVAS_CREATE,
NODE_PROP_UPDATE,
NODE_LABEL_UPDATE
NODE_LABEL_UPDATE,
REL_TYPE_UPDATE
} from 'neo4j-arc/graph-visualization'

import { StyledVisContainer } from './VisualizationView.styled'
Expand Down Expand Up @@ -279,6 +281,37 @@ LIMIT ${maxNewNeighbours}`
}

onGraphInteraction: GraphInteractionCallBack = (event, properties) => {
if (event == REL_TYPE_UPDATE) {
if (properties == null) {
throw new Error(
'A property map with sourceNodeId, targetNodeId, oldType, and newType keys are required'
)
}

const sourceNodeId = properties['sourceNodeId']
const targetNodeId = properties['targetNodeId']
const oldType = properties['oldType']
const newType = properties['newType']

const type = `\`${oldType}\``

const query = `MATCH (source)-[rel:${type}]->(target) WHERE ID(source) = ${sourceNodeId} AND ID(target) = ${targetNodeId} CALL apoc.refactor.setType(rel, '${newType}') YIELD input, output RETURN input, output;`

this.props.bus.self(
CYPHER_REQUEST,
{
query,
params: { sourceNodeId, targetNodeId, oldType, newType },
queryType: NEO4J_BROWSER_USER_ACTION_QUERY
},
(response: any) => {
if (!response.success) {
throw new Error(response.error)
}
}
)
}

if (event == NODE_LABEL_UPDATE) {
if (properties == null) {
throw new Error(
Expand All @@ -292,6 +325,7 @@ LIMIT ${maxNewNeighbours}`

const query = `MATCH(n) WHERE ID(n) = ${nodeId} REMOVE n:${oldLabel} SET n:${newLabel}`
console.log(query)

this.props.bus.self(
CYPHER_REQUEST,
{
Expand All @@ -309,7 +343,9 @@ LIMIT ${maxNewNeighbours}`

if (event == NODE_PROP_UPDATE) {
if (properties == null) {
throw new Error('')
throw new Error(
'A property map with nodeId, propKey, and propVal keys are required'
)
}

const nodeId = properties['nodeId']
Expand All @@ -334,6 +370,34 @@ LIMIT ${maxNewNeighbours}`
)
}

if (event == REL_ON_CANVAS_CREATE) {
if (properties == null) {
throw new Error(
'A property map with sourceNodeId, targetNodeId, and type keys are required'
)
}

const sourceNodeId = properties['sourceNodeId']
const targetNodeId = properties['targetNodeId']
const type = properties['type']

const query = `MATCH (source), (target) WHERE ID(source) = ${sourceNodeId} AND ID(target) = ${targetNodeId} CREATE (source)-[r:\`${type}\` {name: "new link"}]->(target) RETURN type(r);`

this.props.bus.self(
CYPHER_REQUEST,
{
query,
params: { sourceNodeId, targetNodeId, type },
queryType: NEO4J_BROWSER_USER_ACTION_QUERY
},
(response: any) => {
if (!response.success) {
throw new Error(response.error)
}
}
)
}

if (event == NODE_ON_CANVAS_CREATE) {
if (properties == null) {
throw new Error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export function DefaultDetailsPane({
relType: vizItem.item.type
}}
graphStyle={graphStyle}
onGraphInteraction={onGraphInteraction}
sourceNodeId={vizItem.item.source.id}
targetNodeId={vizItem.item.target.id}
/>
)}
{vizItem.type === 'node' &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,54 @@ import React from 'react'

import { GraphStyleModel } from '../../models/GraphStyle'
import { NonClickableRelTypeChip } from './styled'
import {
GraphInteractionCallBack,
REL_TYPE_UPDATE
} from '../Graph/GraphEventHandlerModel'

export type RelTypeProps = {
graphStyle: GraphStyleModel
selectedRelType: { relType: string; propertyKeys: string[]; count?: number }
onGraphInteraction?: GraphInteractionCallBack
sourceNodeId?: string
targetNodeId?: string
}
RelType.defaultProps = {
onGraphInteraction: () => undefined
}
export function RelType({
selectedRelType,
graphStyle
graphStyle,
onGraphInteraction = () => undefined,
sourceNodeId,
targetNodeId
}: RelTypeProps): JSX.Element {
const styleForRelType = graphStyle.forRelationship({
type: selectedRelType.relType
})
return (
<NonClickableRelTypeChip
style={{
backgroundColor: styleForRelType.get('color'),
color: styleForRelType.get('text-color-internal')
}}
<div
suppressContentEditableWarning={true}
contentEditable="true"
onInput={e =>
onGraphInteraction(REL_TYPE_UPDATE, {
sourceNodeId: sourceNodeId,
targetNodeId: targetNodeId,
oldType: selectedRelType.relType,
newType: e.currentTarget.textContent
})
}
>
{selectedRelType.count !== undefined
? `${selectedRelType.relType} (${selectedRelType.count})`
: `${selectedRelType.relType}`}
</NonClickableRelTypeChip>
<NonClickableRelTypeChip
style={{
backgroundColor: styleForRelType.get('color'),
color: styleForRelType.get('text-color-internal')
}}
>
{selectedRelType.count !== undefined
? `${selectedRelType.relType} (${selectedRelType.count})`
: `${selectedRelType.relType}`}
</NonClickableRelTypeChip>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { Visualization } from './visualization/Visualization'
export const NODE_ON_CANVAS_CREATE = 'NODE_ON_CANVAS_CREATE'
export const NODE_PROP_UPDATE = 'NODE_PROP_UPDATE'
export const NODE_LABEL_UPDATE = 'NODE_LABEL_UPDATE'
export const REL_ON_CANVAS_CREATE = 'REL_ON_CANVAS_CREATE'
export const REL_TYPE_UPDATE = 'REL_TYPE_UPDATE'

export type GraphInteraction =
| 'NODE_EXPAND'
Expand All @@ -42,6 +44,8 @@ export type GraphInteraction =
| typeof NODE_ON_CANVAS_CREATE
| typeof NODE_PROP_UPDATE
| typeof NODE_LABEL_UPDATE
| typeof REL_ON_CANVAS_CREATE
| typeof REL_TYPE_UPDATE

export type GraphInteractionCallBack = (
event: GraphInteraction,
Expand All @@ -58,6 +62,9 @@ export class GraphEventHandlerModel {
onGraphInteraction: GraphInteractionCallBack
selectedItem: NodeModel | RelationshipModel | null

private altCreatedRelSourceNode: any
private altCreatedRelTargetNode: any

constructor(
graph: GraphModel,
visualization: Visualization,
Expand All @@ -76,6 +83,9 @@ export class GraphEventHandlerModel {
this.onGraphInteraction = onGraphInteraction ?? (() => undefined)

this.onGraphModelChange = onGraphModelChange

this.altCreatedRelSourceNode = null
this.altCreatedRelTargetNode = null
}

graphModelChanged(): void {
Expand Down Expand Up @@ -274,6 +284,56 @@ export class GraphEventHandlerModel {
})
}

nodeAltDown(node: NodeModel): void {
if (!node) {
return
}

if (
this.altCreatedRelSourceNode == null &&
this.altCreatedRelTargetNode == null
) {
this.altCreatedRelSourceNode = node
} else if (
this.altCreatedRelSourceNode != null &&
this.altCreatedRelTargetNode == null
) {
this.altCreatedRelTargetNode = node

const maxId: number = Math.max(
...this.graph
.relationships()
.map(relationship => parseInt(relationship.id))
)
const newId = maxId + 1

const altCreatedRel: RelationshipModel = new RelationshipModel(
newId.toString(),
this.altCreatedRelSourceNode,
this.altCreatedRelTargetNode,
newId.toString(),
{ name: 'new link' },
{ name: 'string' }
)

this.graph.addRelationships([altCreatedRel])
this.visualization.update({
updateNodes: true,
updateRelationships: true
})
this.graphModelChanged()

this.onGraphInteraction(REL_ON_CANVAS_CREATE, {
type: newId,
sourceNodeId: this.altCreatedRelSourceNode.id,
targetNodeId: this.altCreatedRelTargetNode.id
})

this.altCreatedRelSourceNode = null
this.altCreatedRelTargetNode = null
}
}

bindEventHandlers(): void {
this.visualization
.on('nodeMouseOver', this.onNodeMouseOver.bind(this))
Expand All @@ -289,6 +349,7 @@ export class GraphEventHandlerModel {
.on('nodeClicked', this.nodeClicked.bind(this))
.on('nodeDblClicked', this.nodeDblClicked.bind(this))
.on('nodeUnlock', this.nodeUnlock.bind(this))
.on('nodeAltDown', this.nodeAltDown.bind(this))
this.onItemMouseOut()
}
}
Loading

0 comments on commit 3610689

Please sign in to comment.