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 8f5ba07 commit 7a147db
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 37 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,8 +28,10 @@ import {
GraphInteractionCallBack,
GraphModel,
GraphVisualizer,
REL_ON_CANVAS_CREATE,
NODE_ON_CANVAS_CREATE,
NODE_PROP_UPDATE
NODE_PROP_UPDATE,
REL_TYPE_UPDATE
} from 'neo4j-arc/graph-visualization'

import { StyledVisContainer } from './VisualizationView.styled'
Expand Down Expand Up @@ -278,9 +280,42 @@ 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_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 @@ -305,6 +340,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 @@ -32,6 +32,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 REL_ON_CANVAS_CREATE = 'REL_ON_CANVAS_CREATE'
export const REL_TYPE_UPDATE = 'REL_TYPE_UPDATE'

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

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

private altCreatedRelSourceNode: any
private altCreatedRelTargetNode: any

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

this.onGraphModelChange = onGraphModelChange

this.altCreatedRelSourceNode = null
this.altCreatedRelTargetNode = null
}

graphModelChanged(): void {
Expand Down Expand Up @@ -272,6 +282,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 @@ -287,6 +347,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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ export const nodeEventHandlers = (
trigger('nodeMouseOut', node)
}

const onNodeMouseDown = (_event: KeyboardEvent, node: NodeModel) => {
if (_event.altKey || _event.ctrlKey || _event.metaKey || _event.shiftKey) {
trigger('nodeAltDown', node)
}
}

const dragstarted = (event: D3DragEvent<SVGGElement, NodeModel, any>) => {
initialDragPosition = [event.x, event.y]
restartedSimulation = false
Expand Down Expand Up @@ -115,6 +121,7 @@ export const nodeEventHandlers = (
.on('mouseout', onNodeMouseOut)
.on('click', onNodeClick)
.on('dblclick', onNodeDblClick)
.on('mousedown', onNodeMouseDown)
}

export const relationshipEventHandlers = (
Expand Down
Loading

0 comments on commit 7a147db

Please sign in to comment.