diff --git a/src/components/canvas/canvas.tsx b/src/components/canvas/canvas.tsx index b79d71e..f553e03 100644 --- a/src/components/canvas/canvas.tsx +++ b/src/components/canvas/canvas.tsx @@ -1,6 +1,7 @@ import '@xyflow/react/dist/style.css'; import styled from '@emotion/styled'; import { Background, ProOptions, ReactFlow, ReactFlowProps, useEdgesState, useNodesState } from '@xyflow/react'; +import { useEffect } from 'react'; import { MiniMap } from '@/components/controls/mini-map'; import { Controls } from '@/components/controls/controls'; @@ -11,6 +12,7 @@ import { InternalEdge, InternalNode } from '@/types/internal'; import { FloatingEdge } from '@/components/edge/floating-edge'; import { SelfReferencingEdge } from '@/components/edge/self-referencing-edge'; import { MarkerList } from '@/components/markers/marker-list'; +import { ConnectionLine } from '@/components/line/connection-line'; const MAX_ZOOM = 3; const MIN_ZOOM = 0.1; @@ -34,13 +36,23 @@ const edgeTypes = { selfReferencingEdge: SelfReferencingEdge, }; -type Props = Pick & { nodes: ExternalNode[]; edges: Edge[] }; +type Props = Pick & { nodes: ExternalNode[]; edges: Edge[] }; -export const Canvas = ({ title, nodes: externalNodes, edges: externalEdges }: Props) => { +export const Canvas = ({ title, nodes: externalNodes, edges: externalEdges, onConnect, ...rest }: Props) => { const { initialNodes, initialEdges } = useCanvas(externalNodes, externalEdges); - const [nodes, , onNodesChange] = useNodesState(initialNodes); - const [edges, , onEdgesChange] = useEdgesState(initialEdges); + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + + useEffect(() => { + setNodes(initialNodes); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialNodes]); + + useEffect(() => { + setEdges(initialEdges); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialEdges]); return ( @@ -54,8 +66,11 @@ export const Canvas = ({ title, nodes: externalNodes, edges: externalEdges }: Pr nodes={nodes} onlyRenderVisibleElements={true} edges={edges} + connectionLineComponent={ConnectionLine} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} + onConnect={onConnect} + {...rest} > diff --git a/src/components/canvas/use-canvas.test.tsx b/src/components/canvas/use-canvas.test.tsx index a574ad2..96680d8 100644 --- a/src/components/canvas/use-canvas.test.tsx +++ b/src/components/canvas/use-canvas.test.tsx @@ -6,7 +6,7 @@ import { useCanvas } from './use-canvas'; describe('use-canvas', () => { it('Should get initial nodes', () => { - const { result } = renderHook(() => useCanvas([ORDERS_NODE, EMPLOYEES_NODE], [])); + const { result } = renderHook(() => useCanvas([{ ...ORDERS_NODE, disabled: true }, EMPLOYEES_NODE], [])); expect(result.current.initialNodes).toEqual([ { id: 'orders', @@ -15,12 +15,14 @@ describe('use-canvas', () => { x: 100, y: 100, }, + draggable: false, measured: { height: 36, width: 244, }, data: { borderVariant: undefined, + disabled: true, fields: [ { name: 'ORDER_ID', type: 'varchar', glyphs: ['key'] }, { name: 'SUPPLIER_ID', type: 'varchar', glyphs: ['link'] }, @@ -39,7 +41,9 @@ describe('use-canvas', () => { height: 72, width: 244, }, + draggable: true, data: { + disabled: undefined, borderVariant: undefined, fields: [ { name: 'employeeId', type: 'objectId', glyphs: ['key'] }, diff --git a/src/components/canvas/use-canvas.tsx b/src/components/canvas/use-canvas.tsx index 3ed253a..426eb60 100644 --- a/src/components/canvas/use-canvas.tsx +++ b/src/components/canvas/use-canvas.tsx @@ -7,11 +7,13 @@ export const useCanvas = (externalNodes: ExternalNode[], externalEdges: Edge[]) const initialNodes: InternalNode[] = useMemo( () => externalNodes.map(node => { - const { title, fields, borderVariant, ...rest } = node; + const { title, fields, borderVariant, disabled, ...rest } = node; return { ...rest, + draggable: !disabled, data: { title, + disabled, fields, borderVariant, }, diff --git a/src/components/diagram.stories.tsx b/src/components/diagram.stories.tsx index 87acf65..d5b7bd3 100644 --- a/src/components/diagram.stories.tsx +++ b/src/components/diagram.stories.tsx @@ -1,8 +1,11 @@ import { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; +import { Connection } from '@xyflow/react'; import { Diagram } from '@/components/diagram'; import { EMPLOYEES_NODE, ORDERS_NODE } from '@/mocks/datasets/nodes'; import { EMPLOYEES_TO_EMPLOYEES_EDGE, ORDERS_TO_EMPLOYEES_EDGE } from '@/mocks/datasets/edges'; +import { Edge } from '@/types'; const diagram: Meta = { title: 'Diagram', @@ -18,3 +21,42 @@ const diagram: Meta = { export default diagram; type Story = StoryObj; export const BasicDiagram: Story = {}; +export const DiagramWithConnectableNodes: Story = { + decorators: [ + (Story, context) => { + const [edges, setEdges] = useState(context.args.edges); + const onConnect = (connection: Connection) => { + setEdges([ + ...edges, + { + ...ORDERS_TO_EMPLOYEES_EDGE, + source: connection.source, + target: connection.target, + animated: true, + selected: true, + }, + ]); + }; + + const onPaneClick = () => { + setEdges(edges.filter(edge => edge.id !== ORDERS_TO_EMPLOYEES_EDGE.id)); + }; + + return Story({ + ...context, + args: { + ...context.args, + edges, + onPaneClick, + onConnect, + }, + }); + }, + ], + args: { + title: 'MongoDB Diagram', + isDarkMode: true, + edges: [], + nodes: [ORDERS_NODE, EMPLOYEES_NODE], + }, +}; diff --git a/src/components/line/connection-line.test.tsx b/src/components/line/connection-line.test.tsx new file mode 100644 index 0000000..3747a1b --- /dev/null +++ b/src/components/line/connection-line.test.tsx @@ -0,0 +1,14 @@ +import { render, screen } from '@/mocks/testing-utils'; +import { ConnectionLine } from '@/components/line/connection-line'; + +describe('connection-line', () => { + const renderComponent = () => { + return render(); + }; + + it('Should render line', () => { + renderComponent(); + const path = screen.getByTestId('connection-line'); + expect(path).toHaveAttribute('d', 'M 100,100L 100,100'); + }); +}); diff --git a/src/components/line/connection-line.tsx b/src/components/line/connection-line.tsx new file mode 100644 index 0000000..4f8335f --- /dev/null +++ b/src/components/line/connection-line.tsx @@ -0,0 +1,28 @@ +import { palette } from '@leafygreen-ui/palette'; +import { ConnectionLineComponentProps, getStraightPath } from '@xyflow/react'; + +type Props = Pick; + +export const ConnectionLine = ({ fromX, fromY, toX, toY }: Props) => { + const [edgePath] = getStraightPath({ + sourceX: fromX, + sourceY: fromY, + targetX: toX, + targetY: toY, + }); + + return ( + + + + + ); +}; diff --git a/src/components/node/node.tsx b/src/components/node/node.tsx index baa91c0..5208bfb 100644 --- a/src/components/node/node.tsx +++ b/src/components/node/node.tsx @@ -79,10 +79,25 @@ const NodeHeaderTitle = styled.div` `; const NodeHandle = styled(Handle)` - visibility: hidden; + width: 100%; + height: 100%; + background: blue; + position: absolute; + top: 0; + left: 0; + border-radius: 0; + transform: none; + border: none; + opacity: 0; + z-index: 1; `; -export const Node = ({ type, selected, data: { title, fields, borderVariant, disabled } }: NodeProps) => { +export const Node = ({ + type, + selected, + isConnectable, + data: { title, fields, borderVariant, disabled }, +}: NodeProps) => { const theme = useTheme(); const { zoom } = useViewport(); @@ -126,8 +141,8 @@ export const Node = ({ type, selected, data: { title, fields, borderVariant, dis return (
- - + + {!isContextualZoom && ( diff --git a/src/types/component-props.ts b/src/types/component-props.ts index 03fbca9..f9277b7 100644 --- a/src/types/component-props.ts +++ b/src/types/component-props.ts @@ -3,7 +3,7 @@ import { ReactFlowProps } from '@xyflow/react'; import { Edge } from '@/types/edge'; import { Node } from '@/types/node'; -type BaseProps = Pick; +type BaseProps = Pick; export interface DiagramProps extends BaseProps { isDarkMode?: boolean; diff --git a/src/types/edge.ts b/src/types/edge.ts index 17433b1..6aaeaa5 100644 --- a/src/types/edge.ts +++ b/src/types/edge.ts @@ -1,6 +1,9 @@ import { Edge as ReactFlowEdge } from '@xyflow/react'; -export type BaseEdgeProps = Pick; +export type BaseEdgeProps = Pick< + ReactFlowEdge, + 'id' | 'type' | 'source' | 'target' | 'hidden' | 'selected' | 'animated' +>; export type Marker = 'one' | 'many' | 'oneOrMany'; diff --git a/src/types/node.ts b/src/types/node.ts index 4e962a2..b769cdf 100644 --- a/src/types/node.ts +++ b/src/types/node.ts @@ -4,7 +4,7 @@ import { NodeData } from '@/types/internal'; export type BaseNodeProps = Pick< ReactFlowNode<{}, NodeType>, - 'id' | 'type' | 'position' | 'hidden' | 'draggable' | 'selected' | 'style' | 'className' | 'measured' + 'id' | 'type' | 'position' | 'hidden' | 'draggable' | 'connectable' | 'selected' | 'style' | 'className' | 'measured' >; export type NodeType = 'table' | 'collection'; export type NodeBorderVariant = 'subtle' | 'preview' | 'selected' | 'none';