Skip to content

Feat/drag n drop #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
10 changes: 10 additions & 0 deletions src/main/react/app/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,14 @@ body {
/* Allotment Styling */
--focus-border: var(--color-brand);
--separator-border: var(--color-gray-200);
/* Node Styling */
--type-pipe: #68D250;
--type-listener: #D250BF;
--type-receiver: #D250BF;
--type-sender: #30CCAF;
--type-validator: #3079CC;
--type-wrapper: #4A30CC;
--type-job: #E0DE54;
--type-exit: #E84E4E;
--type-default: #FDC300;
}
13 changes: 11 additions & 2 deletions src/main/react/app/routes/builder/builder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import SidebarIcon from '/icons/solar/Sidebar Minimalistic.svg?react'
import BuilderContext from '~/routes/builder/context/builder-context'
import Flow from '~/routes/builder/canvas/flow'
import FolderIcon from '/icons/solar/Folder.svg?react'
import NodeContext from "~/routes/builder/context/node-context";
import useNodeContextStore from "~/stores/node-context-store";

const tabs = {
tab1: { value: 'tab1', icon: FolderIcon },
Expand Down Expand Up @@ -33,6 +35,7 @@ export default function Builder() {
const [selectedTab, setSelectedTab] = useState<string | undefined>()

const [visible, setVisible] = useState([true, true, true])
const [showNodeContext, setShowNodeContext] = useState(false)
const [hasReadFromLocalStorage, setHasReadFromLocalStorage] = useState(false)
const [sizes, setSizes] = useState<number[]>([])

Expand Down Expand Up @@ -76,6 +79,8 @@ export default function Builder() {
onVisibleChangeHandler(index, !visible[index])
}

const nodeId = useNodeContextStore((state) => state.nodeId)

return (
<>
{hasReadFromLocalStorage && (
Expand Down Expand Up @@ -120,7 +125,7 @@ export default function Builder() {
<div className="h-12 border-b border-b-gray-200">
Path: {Object.entries(tabs).find(([key]) => key === selectedTab)?.[1]?.value}
</div>
<Flow />
<Flow onDragEnd={setShowNodeContext} />
</Allotment.Pane>
<Allotment.Pane
key="right"
Expand All @@ -130,7 +135,11 @@ export default function Builder() {
preferredSize={300}
visible={visible[SidebarIndex.RIGHT]}
>
<BuilderContext onClose={toggleRightVisible} />
{showNodeContext ? (
<NodeContext nodeId={nodeId} setShowNodeContext={setShowNodeContext} />
) : (
<BuilderContext onClose={toggleRightVisible} />
)}
</Allotment.Pane>
</Allotment>
)}
Expand Down
6 changes: 6 additions & 0 deletions src/main/react/app/routes/builder/canvas/flow.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const FlowConfig = {
NODE_DEFAULT_WIDTH: 300,
NODE_DEFAULT_HEIGHT: 200,
EXIT_DEFAULT_WIDTH: 150,
EXIT_DEFAULT_HEIGHT: 100,
}
66 changes: 61 additions & 5 deletions src/main/react/app/routes/builder/canvas/flow.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { Background, BackgroundVariant, Controls, type Node, ReactFlow } from '@xyflow/react'
import {
Background,
BackgroundVariant,
Controls,
type Node,
ReactFlow,
ReactFlowProvider,
useReactFlow,
} from '@xyflow/react'
import '@xyflow/react/dist/style.css'
import FrankNodeComponent, { type FrankNode } from '~/routes/builder/canvas/nodetypes/frank-node'
import FrankEdgeComponent from '~/routes/builder/canvas/frank-edge'
import ExitNodeComponent from '~/routes/builder/canvas/nodetypes/exit-node'
import ExitNodeComponent, { type ExitNode } from '~/routes/builder/canvas/nodetypes/exit-node'
import StartNodeComponent, { type StartNode } from '~/routes/builder/canvas/nodetypes/start-node'
import useFlowStore, { type FlowState } from '~/stores/flow-store'
import { useShallow } from 'zustand/react/shallow'
import { FlowConfig } from '~/routes/builder/canvas/flow.config'
import { getElementTypeFromName } from '~/routes/builder/node-translator-module'

export type FlowNode = FrankNode | StartNode | Node
export type FlowNode = FrankNode | StartNode | ExitNode | Node
const selector = (state: FlowState) => ({
nodes: state.nodes,
edges: state.edges,
Expand All @@ -16,14 +26,52 @@ const selector = (state: FlowState) => ({
onConnect: state.onConnect,
})

export default function Flow() {
function FlowCanvas({ onDragEnd }: Readonly<{ onDragEnd: (b: boolean) => void }>) {
const nodeTypes = { frankNode: FrankNodeComponent, exitNode: ExitNodeComponent, startNode: StartNodeComponent }
const edgeTypes = { frankEdge: FrankEdgeComponent }
const reactFlow = useReactFlow()

const { nodes, edges, onNodesChange, onEdgesChange, onConnect } = useFlowStore(useShallow(selector))

const onDragOver = (event: React.DragEvent) => {
event.preventDefault()
event.dataTransfer.dropEffect = 'move'
}

const onDrop = (event: React.DragEvent) => {
event.preventDefault()
onDragEnd(true)

const data = event.dataTransfer.getData('application/reactflow')
if (!data) return

const parsedData = JSON.parse(data)
const { screenToFlowPosition } = reactFlow

const position = screenToFlowPosition({ x: event.clientX, y: event.clientY })
const newId = useFlowStore.getState().nodes.length
const elementType = getElementTypeFromName(parsedData.name)
const nodeType = elementType == 'exit' ? 'exitNode' : 'frankNode'
const newNode: FrankNode = {
id: newId.toString(),
position: {
x: position.x - (nodeType == 'exitNode' ? FlowConfig.EXIT_DEFAULT_WIDTH : FlowConfig.NODE_DEFAULT_WIDTH) / 2, // Centers node on top of cursor
y: position.y - (nodeType == 'exitNode' ? FlowConfig.EXIT_DEFAULT_HEIGHT : FlowConfig.NODE_DEFAULT_HEIGHT) / 2,
},
data: {
subtype: parsedData.name,
type: elementType,
name: ``,
sourceHandles: [{ type: 'success', index: 1 }],
children: [],
},
type: nodeType,
}
useFlowStore.getState().setNodes([...nodes, newNode])
}

return (
<div style={{ height: '100%' }}>
<div style={{ height: '100%' }} onDrop={onDrop} onDragOver={onDragOver}>
<ReactFlow
fitView
nodes={nodes}
Expand All @@ -40,3 +88,11 @@ export default function Flow() {
</div>
)
}

export default function Flow({ onDragEnd }: Readonly<{ onDragEnd: (b: boolean) => void }>) {
return (
<ReactFlowProvider>
<FlowCanvas onDragEnd={onDragEnd} />
</ReactFlowProvider>
)
}
2 changes: 1 addition & 1 deletion src/main/react/app/routes/builder/canvas/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const initialNodes: Node[] = [
{ type: 'failure', index: 2 },
],
attributes: {
xpathExpression: "concat('{&quot;temperature&quot;:', /current/temperature/@value, '}')",
xpathExpression: "concat('{&quot;temperature&quot;:', /current/temperature/@value, '}')",
},
children: [],
},
Expand Down
29 changes: 23 additions & 6 deletions src/main/react/app/routes/builder/canvas/nodetypes/exit-node.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
import { Handle, type NodeProps, NodeResizeControl, Position } from '@xyflow/react'
import { type FrankNode, ResizeIcon, translateTypeToColor } from '~/routes/builder/canvas/nodetypes/frank-node'
import { Handle, type Node, type NodeProps, NodeResizeControl, Position } from '@xyflow/react'
import { ResizeIcon } from '~/routes/builder/canvas/nodetypes/frank-node'
import { FlowConfig } from '~/routes/builder/canvas/flow.config'
import { useState } from 'react'

export default function ExitNode(properties: NodeProps<FrankNode>) {
const minNodeWidth = 150
const minNodeHeight = 100
export type ExitNode = Node<{
subtype: string
type: string
name: string
}>

export default function ExitNode(properties: NodeProps<ExitNode>) {
const minNodeWidth = FlowConfig.EXIT_DEFAULT_WIDTH
const minNodeHeight = FlowConfig.EXIT_DEFAULT_HEIGHT

const [dimensions, setDimensions] = useState({
width: minNodeWidth, // Initial width
height: minNodeHeight, // Initial height
})

return (
<>
<NodeResizeControl
minWidth={minNodeWidth}
minHeight={minNodeHeight}
onResize={(event, data) => {
setDimensions({ width: data.width, height: data.height })
}}
style={{
background: 'transparent',
border: 'none',
Expand All @@ -24,14 +40,15 @@ export default function ExitNode(properties: NodeProps<FrankNode>) {
style={{
minHeight: `${minNodeHeight}px`,
minWidth: `${minNodeWidth}px`,
width: `${dimensions.width}px`,
}}
>
<div
className="box-border w-full rounded-t-md p-1"
style={{
background: `radial-gradient(
ellipse at top left,
${translateTypeToColor(properties.data.type)} 0%,
var(--type-exit) 0%,
white 70%
)`,
}}
Expand Down
43 changes: 11 additions & 32 deletions src/main/react/app/routes/builder/canvas/nodetypes/frank-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
import useFlowStore from '~/stores/flow-store'
import { CustomHandle } from '~/components/flow/handle'
import { FlowConfig } from '~/routes/builder/canvas/flow.config'

export interface ChildNode {
subtype: string
Expand All @@ -27,33 +28,11 @@ export type FrankNode = Node<{
children: ChildNode[]
}>

export function translateTypeToColor(type: string): string {
switch (type.toLowerCase()) {
case 'pipe': {
return '#68D250'
}
case 'listener': {
return '#D250BF'
}
case 'receiver': {
return '#D250BF'
}
case 'sender': {
return '#30CCAF'
}
case 'exit': {
return '#E84E4E'
}
default: {
return '#FDC300'
}
}
}

export default function FrankNode(properties: NodeProps<FrankNode>) {
const minNodeWidth = 300
const minNodeHeight = 200
const bgColor = translateTypeToColor(properties.data.type)
const minNodeWidth = FlowConfig.NODE_DEFAULT_WIDTH
const minNodeHeight = FlowConfig.NODE_DEFAULT_HEIGHT
const type = properties.data.type.toLowerCase()
const colorVariable = `--type-${type}`
const handleSpacing = 20
const containerReference = useRef<HTMLDivElement>(null)

Expand Down Expand Up @@ -94,15 +73,15 @@ export default function FrankNode(properties: NodeProps<FrankNode>) {
[properties.id, properties.data.sourceHandles.length],
)

const openMenu = (event: React.MouseEvent) => {
const toggleMenu = (event: React.MouseEvent) => {
const { clientX, clientY } = event
const { screenToFlowPosition } = reactFlow
const flowPosition = screenToFlowPosition({ x: clientX, y: clientY })
const adjustedX = flowPosition.x - properties.positionAbsoluteX
const adjustedY = flowPosition.y - properties.positionAbsoluteY

setMenuPosition({ x: adjustedX, y: adjustedY })
setIsMenuOpen(true)
setIsMenuOpen(!isMenuOpen)
}

return (
Expand Down Expand Up @@ -136,7 +115,7 @@ export default function FrankNode(properties: NodeProps<FrankNode>) {
style={{
background: `radial-gradient(
ellipse at top left,
${bgColor} 0%,
var(${colorVariable}) 0%,
white 70%
)`,
}}
Expand Down Expand Up @@ -167,9 +146,9 @@ export default function FrankNode(properties: NodeProps<FrankNode>) {
style={{
background: `radial-gradient(
ellipse at top left,
${translateTypeToColor(child.type)} 0%,
var(--type-${child.type?.toLowerCase?.() || 'default'}) 0%,
white 70%
)`,
)`,
}}
>
<h1 className="font-bold">{child.subtype}</h1>
Expand Down Expand Up @@ -216,7 +195,7 @@ export default function FrankNode(properties: NodeProps<FrankNode>) {
))}
<div
onClick={(event) => {
openMenu(event)
toggleMenu(event)
}}
className="absolute right-[-23px] h-[15px] w-[15px] cursor-pointer justify-center rounded-full border bg-gray-400 text-center text-[8px] font-bold text-white"
style={{
Expand Down
23 changes: 22 additions & 1 deletion src/main/react/app/routes/builder/context/builder-context.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import SidebarIcon from '/icons/solar/Sidebar Minimalistic.svg?react'
import MagnifierIcon from '/icons/solar/Magnifier.svg?react'
import useFrankDocStore from '~/stores/frank-doc-store'
import useNodeContextStore from '~/stores/node-context-store'
import {useReactFlow} from "@xyflow/react";
import useFlowStore from "~/stores/flow-store";

export default function BuilderContext({ onClose }: Readonly<{ onClose: () => void }>) {
const { frankDocRaw, isLoading, error } = useFrankDocStore()
const { setAttributes, setNodeId } = useNodeContextStore((state) => state)
const nodes = useFlowStore((state) => state.nodes)

const onDragStart = (value: { attributes: any[] }) => {
return (event: {
dataTransfer: { setData: (argument0: string, argument1: string) => void; effectAllowed: string }
}) => {
setAttributes(value.attributes)
setNodeId(nodes.length)
event.dataTransfer.setData('application/reactflow', JSON.stringify(value))
event.dataTransfer.effectAllowed = 'move'
}
}

return (
<div className="h-full">
Expand Down Expand Up @@ -31,7 +47,12 @@ export default function BuilderContext({ onClose }: Readonly<{ onClose: () => vo
{error && <li>Error: {error}</li>}
{!isLoading &&
Object.entries(frankDocRaw?.elements).map(([key, value]: [string, any]) => (
<li className="m-2 rounded border border-gray-400 p-4" key={value.name}>
<li
className="m-2 cursor-move rounded border border-gray-400 p-4"
key={value.name}
draggable
onDragStart={onDragStart(value)}
>
{value.name}
</li>
))}
Expand Down
Loading
Loading