Skip to content

Commit

Permalink
feat: add drag and drop functionality to chart
Browse files Browse the repository at this point in the history
  • Loading branch information
davec504 committed Apr 20, 2024
1 parent 1ebb095 commit a07bd15
Show file tree
Hide file tree
Showing 13 changed files with 664 additions and 48 deletions.
116 changes: 92 additions & 24 deletions src/Components/Diagram/Diagram.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FC, useCallback, useEffect } from 'react'
import ReactFlow, { Background, BackgroundVariant, Connection, Controls, MiniMap, addEdge, useEdgesState, useNodesState } from 'reactflow'
import React, { FC, useCallback, useEffect, useState } from 'react'
import ReactFlow, { Background, BackgroundVariant, Connection, Controls, MiniMap, useEdgesState, useNodesState, Node, Edge } from 'reactflow'
import { withInjection } from '../../Core/Providers/injection'
import { RdfInstancePresenter } from '../../rdfInstanceViewer/RdfInstancePresenter'
import { RdfPanelProps } from '../../types'
Expand All @@ -9,6 +9,8 @@ import ClassInstanceNode from '../../lib/CustomNode/ClassInstanceNode'
import { observer } from 'mobx-react'
import CustomEdge from '../../lib/CustomEdge/CustomEdge'
import getLayoutNodes from './Layout'
import { NodeDialog } from './NodeDialog'
import { EdgeDialog } from './EdgeDialog'

const nodeTypes = {
classInstanceNode: ClassInstanceNode,
Expand All @@ -21,6 +23,8 @@ const edgeTypes = {
const DiagramComponent: FC<RdfPanelProps> = observer((props: RdfPanelProps) => {
const [nodes, setNodes, onNodesChange] = useNodesState(props.presenter.viewModel.nodes)
const [edges, setEdges, onEdgesChange] = useEdgesState([])
const [open, setOpen] = useState(false)
const [edgeDialogOpen, setEdgeDialogOpen] = useState(false)

useEffect(() => {
if (!props.presenter) {
Expand All @@ -31,37 +35,101 @@ const DiagramComponent: FC<RdfPanelProps> = observer((props: RdfPanelProps) => {

useEffect(() => {
const { nodes, edges } = getLayoutNodes(props.presenter.viewModel.nodes, props.presenter.viewModel.edges)
console.log({ nodes, edges });

setNodes(nodes)
setEdges(edges)
}, [props.presenter.viewModel.nodes, props.presenter.viewModel.edges, setEdges, setNodes])

const onDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault()
event.dataTransfer.dropEffect = "move"
}, [])

const onDrop = useCallback((event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault()

const type = event.dataTransfer.getData('application/reactflow');

// check if the dropped element is valid
if (typeof type === 'undefined' || !type) {
return;
}

props.presenter.newNodeType = type
setOpen(true)

}, [])

const onConnect = useCallback((params: Connection) => {
if (!params.source || !params.target) return
props.presenter.newEdgeSource = params.source
props.presenter.newEdgeTarget = params.target
setEdgeDialogOpen(true)
}, [setEdgeDialogOpen])



const onCloseDialog = () => {
setOpen(false)
setEdgeDialogOpen(false)
}

const onNodesDelete = (nodes: Array<Node>) => {
props.presenter.deleteNodeAndAssociatedEdges(nodes[0].id)
}

const onEdgesDelete = (edges: Array<Edge>) => {
console.log({ edges })
if (edges.length === 0) return
const edge = edges[0]
if (!edge.label || !edge.target || !edge.source) {
console.warn("cannot delete edge, missing information")
return
}

props.presenter.deleteEdge(edge.source, edge.target, edge.label as string)
}

const onSubmitNode = (name: string): void => {
props.presenter.newNodeName = name
props.presenter.addNode()

setOpen(false)
}

const onSubmitEdge = (type: string): void => {
props.presenter.newEdgeType = type
props.presenter.addEdge()
setEdgeDialogOpen(false)
}

return (<>
<ReactFlow
fitView
panOnDrag
nodes={nodes}
edges={edges}
onConnect={onConnect}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
onDragOver={onDragOver}
onDrop={onDrop}
onNodesDelete={onNodesDelete}
onEdgesDelete={onEdgesDelete}
edgeTypes={edgeTypes}
>
<Controls />
<MiniMap style={
{ backgroundColor: 'gray' }
} />
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
</ReactFlow>
{open && <NodeDialog title="Add node details:" onClose={onCloseDialog} options={Object.keys(props.presenter.viewModel.prefixes)} onSubmit={onSubmitNode} />}
{edgeDialogOpen && <EdgeDialog title="Add edge details:" onClose={onCloseDialog} options={props.presenter.viewModel.relationships} onSubmit={onSubmitEdge} />}
</>
)

props.presenter.rdfInstanceRepository.addEdgeToRdf(params.source, params.target)
setEdges((eds) => addEdge(params, eds))
}, [setEdges])

return (<ReactFlow
fitView
panOnDrag
nodes={nodes}
edges={edges}
onConnect={onConnect}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
>
<Controls />
<MiniMap style={
{ backgroundColor: 'gray' }
} />
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
</ReactFlow>)
})

// export default Diagram
Expand Down
42 changes: 42 additions & 0 deletions src/Components/Diagram/EdgeDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { FC, useState } from 'react'
import { TeliAutocomplete, TeliButton } from "@telicent-oss/ds"
import { DialogBox } from '../../lib/DialogBox/DialogBox'

interface EdgeDialogProps {
onClose: () => void
onSubmit: (name: string) => void
options: Array<string>
title: string
}
export const EdgeDialog: FC<EdgeDialogProps> = ({ options, onClose, title, onSubmit }) => {
const [selectedEdgeType, setSelectedEdgeType] = useState<string | null>(null)

const onChangePrefix = (event: React.SyntheticEvent<Element, Event>, value: string | null) => {
event.preventDefault()
if (!value) {
console.warn("Invalid value", value)
}
setSelectedEdgeType(value)
}

const onHandleSubmit = () => {
if (!selectedEdgeType) {
console.warn("Edge must have valid inputs")
return
}
onSubmit(selectedEdgeType)
}

return (
<DialogBox onClose={onClose} title={title}>
<div className="dark:text-whiteSmoke flex flex-col gap-y-8 rounded">
<div className='flex gap-x-2'>
<TeliAutocomplete options={options} width={150} label="Edge Type:" onChange={onChangePrefix} />
</div>
<div className='flex justify-end w-full'>
<TeliButton onClick={onHandleSubmit} variant="secondary" disabled={!selectedEdgeType}>Submit</TeliButton>
</div>
</div>
</DialogBox>
)
}
4 changes: 0 additions & 4 deletions src/Components/Diagram/Layout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,11 @@ const setNodeSizes = (dagreGraph: dagre.graphlib.Graph) => (node: Node) => {
};

const setEdgeLink = (dagreGraph: dagre.graphlib.Graph) => (edge: Edge) => {
console.log({ edge });

dagreGraph.setEdge(edge.source, edge.target);
};

const calcNodePosition = (dagreGraph: dagre.graphlib.Graph, direction: "TB" | "LR") => (node: Node) => {
console.log({ calc: node })
const nodeWithPosition = dagreGraph.node(node.id) as dagre.Node;
console.log({ nodeWithPosition });

node.targetPosition = direction === "TB" ? Position.Top : Position.Left;
node.sourcePosition = direction === "TB" ? Position.Bottom : Position.Right;
Expand Down
55 changes: 55 additions & 0 deletions src/Components/Diagram/NodeDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { FC, useState } from 'react'
import { TeliAutocomplete, TeliButton, TeliTextField } from "@telicent-oss/ds"
import { DialogBox } from '../../lib/DialogBox/DialogBox'

interface NodeDialogProps {
onClose: () => void
onSubmit: (name: string) => void
options: Array<string>
title: string
}
export const NodeDialog: FC<NodeDialogProps> = ({ options, onClose, title, onSubmit }) => {
const [selectedPrefix, setSelectedPrefix] = useState<string>(options[0])
const [name, setName] = useState<string>(crypto.randomUUID())

const onChangeName: React.ChangeEventHandler<HTMLInputElement> = (event) => {
event.preventDefault()
if (!event.target.value) {
setName(crypto.randomUUID())
return
}

setName(event.target.value)
}

const onChangePrefix = (event: React.SyntheticEvent<Element, Event>, value: string | null) => {
event.preventDefault()
if (!value) {
console.warn("Invalid value", value)
return
}
setSelectedPrefix(value)
}

const onHandleSubmit = () => {
if (!selectedPrefix || !name) {
console.warn("Node must have valid inputs")
return
}
onSubmit(`${selectedPrefix}${name}`)
}

return (
<DialogBox onClose={onClose} title={title}>
<div className="dark:text-whiteSmoke flex flex-col gap-y-8 rounded">
<div className='flex gap-x-2'>
<TeliAutocomplete options={options} width={150} label="Prefix" onChange={onChangePrefix} />
<TeliTextField id="node-name" label="Node name" onChange={onChangeName} value={name} required />
</div>
<div className='flex justify-end w-full'>
<TeliButton onClick={onHandleSubmit} variant="secondary" disabled={!name || !selectedPrefix}>Submit</TeliButton>
</div>
</div>
</DialogBox>
)
}
22 changes: 13 additions & 9 deletions src/Components/HierarchyMenu/HierarchyMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,29 @@ const MenuItemComponent: React.FC<MenuItemProps> = ({ item, level = 0 }) => {
setIsExpanded(!isExpanded);
};

const onDragStart = (event: React.DragEvent<HTMLDivElement>, nodeType: string) => {
event.dataTransfer.setData('application/reactflow', nodeType);
event.dataTransfer.effectAllowed = 'move';
}

const paddingLeft = 20 * level; // Adjust the padding for indentation

return (
<li className='text-left'>
<div
className={`p-2 pl-${paddingLeft} flex gap-x-4 items-center`}
className={`p-1 l-${paddingLeft} flex gap-x-4 items-center`}
onClick={toggleExpand}
>
{item.children?.length > 0 && <i className={classNames("fa-solid fa-chevron-right ease-in-out duration-200", {
"transform: rotate-90": isExpanded,
"transform: rotate-0": !isExpanded
})} />}
<div className={classNames("cursor-grab focus:cursor-grabbing flex items-center gap-x-1", {
"pl-7": item.children?.length === 0
})}>
<i className={classNames("fa-solid fa-grip-vertical",
<div className={classNames("cursor-grab focus:cursor-grabbing flex items-center gap-x-1 bg-[#141414] border-[#373737] text-sm p-1 border rounded hover:border-[#cccccc] text-[#373737] hover:text-[#cccccc]", {
"ml-7": item.children?.length === 0
})} onDragStart={(event) => onDragStart(event, item.name)} draggable>
<i className={classNames("fa-solid fa-ellipsis-vertical",
)} />
<span className={`${pascalToKebab(getWordAfterLastHash(item.name))} hover:font-bold`}>{item.label}</span></div>
<span className={`${pascalToKebab(getWordAfterLastHash(item.name))}`}>{item.label}</span></div>
</div>
{isExpanded && item.children && (
<ul className="ml-4">
Expand Down Expand Up @@ -86,7 +91,6 @@ const HierarchyMenuComponent: React.FC<HierarchyProps> = observer((props) => {
return
}
props.presenter.hierarchyRepository.loadHierarchy()
console.log(props.presenter)
}, [])

useEffect(() => {
Expand All @@ -97,8 +101,8 @@ const HierarchyMenuComponent: React.FC<HierarchyProps> = observer((props) => {
return (
<>
<CompactMenu isOpen={isOpen} onClose={toggle} />
<div className={`side-drawer absolute inset-y-0 left-0 w-1/6 bg-black-100 transform drop-shadow-xl shadow-black-500 transition-transform ease-in-out z-20 duration-300 ${isOpen ? 'translate-x-0' : '-translate-x-64'}`}>
<div className="side-drawer-content p-4">
<div className={`side-drawer absolute inset-y-0 left-0 w-1/5 bg-black-100 transform drop-shadow-xl shadow-black-500 transition-transform ease-in-out z-20 duration-300 ${isOpen ? 'translate-x-0' : '-translate-x-64'}`}>
<div className="side-drawer-content p-4 overflow-auto h-full">
{menu && <Menu item={menu} />}
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion src/Components/HierarchyMenu/HierarchyPresenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export class HierarchyPresenter {

get viewModel() {
const hierarchy = JSON.parse(JSON.stringify(this.hierarchyRepository.hierarchyPm))
console.log({ hierarchy })
return {
hierarchy,
hasHierarchy: Object.keys(hierarchy).length > 0
Expand Down
1 change: 0 additions & 1 deletion src/Components/HierarchyMenu/HierarchyRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ export class HierarchyRepository {
try {
const statements = await this.dataGateway.get<z.infer<typeof HierarchyResponseSchema>>(query, getAndCheckHierarchyResponse)

console.log({ statements })
const getOrCreateHierarchy = (
hierarchy: Record<string, HierarchyClass>,
key: string
Expand Down
4 changes: 2 additions & 2 deletions src/Components/ResizableDivs/ResizableDivs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ const ResizableDivs: React.FC<ResizableDivsProps> = ({ children }) => {

const [left, right] = React.Children.toArray(children)
const [isResizing, setIsResizing] = useState(false);
const [div1Width, setDiv1Width] = useState<{ width: string }>({ width: '50%' });
const [div2Width, setDiv2Width] = useState<{ width: string }>({ width: '50%' });
const [div1Width, setDiv1Width] = useState<{ width: string }>({ width: '70%' });
const [div2Width, setDiv2Width] = useState<{ width: string }>({ width: '30%' });
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand Down
2 changes: 0 additions & 2 deletions src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ export const formatEdge = (edge: Quad, label: string): Edge => {
}
})

console.log({ newEdge });

return newEdge
}

Expand Down
4 changes: 1 addition & 3 deletions src/lib/CustomEdge/CustomEdge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ const CustomEdge: FC<EdgeProps> = ({ id, sourceX, sourceY, targetX, targetY, sou
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
background: '#ffcc00',
padding: 10,
borderRadius: 5,
fontSize: 12,
fontWeight: 700,
}}
className="nodrag nopan"
className="nodrag nopan p-2 px-2 bg-gray-800"
>
{label}
</div>
Expand Down
18 changes: 18 additions & 0 deletions src/lib/DialogBox/DialogBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React, { FC } from 'react'

interface DialogBoxProps {
title: string
onClose: () => void
children: React.ReactNode
}
export const DialogBox: FC<DialogBoxProps> = ({ onClose, children, title }) => {
return (
<>
<div className="absolute top-0 left-0 h-full w-full z-40 bg-black-100 opacity-80" onClick={onClose}></div>
<div className="bg-black-100 z-50 absolute top-24 left-1/2 transform -translate-x-1/2 py-3 px-6 rounded border-black-400 shadow-black-300 shadow-md border" >
<h2 className='text-left pb-6'>{title}</h2>
{children}
</div>
</>
)
}
Loading

0 comments on commit a07bd15

Please # to comment.