Skip to content

Commit

Permalink
Use children in Edge/Port components to display custom components (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vadorequest committed Mar 8, 2021
1 parent b72130b commit 1a7d418
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 40 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"react-dom": "17.0.1",
"react-select": "4.0.2",
"react-textarea-autosize": "8.3.0",
"reaflow": "3.0.13",
"reaflow": "3.0.14",
"recoil": "0.1.2",
"recoil-devtools-dock": "^0.1.6",
"recoil-devtools-log-monitor": "^0.2.7",
Expand Down
51 changes: 32 additions & 19 deletions src/components/edges/BaseEdge.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Button } from '@chakra-ui/react';
import { css } from '@emotion/react';
import classnames from 'classnames';
import cloneDeep from 'lodash.clonedeep';
import now from 'lodash.now';
import React from 'react';
import {
Edge,
EdgeChildProps,
EdgeData,
} from 'reaflow';
import {
Expand Down Expand Up @@ -155,25 +158,35 @@ const BaseEdge: React.FunctionComponent<Props> = (props) => {
/>
)}
onClick={onEdgeClick}
/>
// Doesn't support children - See https://github.com/reaviz/reaflow/issues/67
// Possible to use a custom children to achieve this, but not great DX because of manual x/y placement
/*
<foreignObject
width={30}
height={30}
// x={1272}
// y={231}
css={css`
position: absolute;
//left: 1272px;
//top: 231px;
color: black;
`}
>
test
</foreignObject>
* */
>
{
(edgeChildProps: EdgeChildProps) => {
const {
center,
} = edgeChildProps;

return (
<foreignObject
width={100} // Content width will be limited by the width of the foreignObject
height={60}
x={center?.x}
y={center?.y}
css={css`
position: absolute;
color: black;
z-index: 1;
`}
>
{
isSelected && (
<Button>Edit label</Button>
)
}
</foreignObject>
);
}
}
</Edge>
);
};

Expand Down
2 changes: 2 additions & 0 deletions src/components/editor/PlaygroundContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { css } from '@emotion/react';
import React, { MutableRefObject } from 'react';
import { CanvasRef } from 'reaflow';
import BlockPickerMenu from '../blocks/BlockPickerMenu';
import AbsoluteTooltip from '../plugins/AbsoluteTooltip';
import CanvasContainer from './CanvasContainer';

type Props = {
Expand Down Expand Up @@ -45,6 +46,7 @@ const PlaygroundContainer: React.FunctionComponent<Props> = (props): JSX.Element
/>

<BlockPickerMenu />
<AbsoluteTooltip />
</div>
);
};
Expand Down
26 changes: 11 additions & 15 deletions src/components/nodes/BaseNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
} from '../../utils/nodes';
import { createPort } from '../../utils/ports';
import BasePort from '../ports/BasePort';
import BasePortChild from '../ports/BasePortChild';

type Props = BaseNodeProps & {
hasCloneAction?: boolean;
Expand Down Expand Up @@ -270,7 +271,16 @@ const BaseNode: BaseNodeComponent<Props> = (props) => {
onKeyDown={onKeyDown}
onRemove={onNodeRemove}
remove={(<Remove hidden={true} />)}
port={(<BasePort fromNodeId={node.id} />)}
port={(
<BasePort
fromNodeId={node.id}
additionalPortChildProps={{
fromNode: node,
isNodeReachable: isReachable,
}}
PortChildComponent={BasePortChild}
/>
)}
>
{
/**
Expand Down Expand Up @@ -413,20 +423,6 @@ const BaseNode: BaseNodeComponent<Props> = (props) => {
<div
className={`node-content-container ${nodeType}-content-container`}
>
{
// Displays a warning icon at the left of the node's title (using css float to avoid break line)
!isReachable && (
<span
className={'is-unreachable-warning'}
>
<FontAwesomeIcon
icon={['fas', 'exclamation-triangle']}
onClick={() => alert(`This node is not reachable because there are no edge connected to its entry port.`)}
/>
</span>
)
}

{
// Invoke the children as a function, or render the children as a component, if it's not a function
// @ts-ignore
Expand Down
43 changes: 43 additions & 0 deletions src/components/plugins/AbsoluteTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Tooltip } from '@chakra-ui/react';
import { css } from '@emotion/react';
import React from 'react';
import { useRecoilState } from 'recoil';
import { absoluteTooltipState } from '../../states/absoluteTooltipState';

type Props = {};

/**
* Tooltip displayed in absolute position.
*
* Displays on top of the canvas.
*/
export const AbsoluteTooltip: React.FunctionComponent<Props> = (props) => {
const [tooltip, setTooltip] = useRecoilState(absoluteTooltipState);

return (
<div
className={'absolute-tooltip'}
css={css`
// ChakraUI Tooltip component doesn't support absolute position natively
// This component wraps the <Tooltip> component and display it in absolute position
position: absolute;
left: ${(tooltip?.x || 0)}px;
top: ${(tooltip?.y || 0)}px;
`}
>
{
tooltip?.isDisplayed && (
<Tooltip
label={tooltip?.text}
// Automatically opens the tooltip (uncontrolled)
isOpen={true}
>
&nbsp;
</Tooltip>
)
}
</div>
);
};

export default AbsoluteTooltip;
21 changes: 20 additions & 1 deletion src/components/ports/BasePort.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import now from 'lodash.now';
import React from 'react';
import {
Port,
PortChildProps,
PortSide,
} from 'reaflow';
import {
Expand All @@ -25,6 +26,7 @@ import { selectedEdgesSelector } from '../../states/selectedEdgesState';
import { selectedNodesSelector } from '../../states/selectedNodesState';
import BaseEdgeData from '../../types/BaseEdgeData';
import BaseNodeData from '../../types/BaseNodeData';
import BasePortChildProps, { AdditionalPortChildProps } from '../../types/BasePortChildProps';
import BasePortData from '../../types/BasePortData';
import BasePortProps from '../../types/BasePortProps';
import BlockPickerMenu, { OnBlockClick } from '../../types/BlockPickerMenu';
Expand All @@ -45,6 +47,8 @@ import {

type Props = {
fromNodeId: string;
additionalPortChildProps: AdditionalPortChildProps;
PortChildComponent: React.FunctionComponent<BasePortChildProps>;
} & BasePortProps;

/**
Expand All @@ -62,6 +66,8 @@ const BasePort: React.FunctionComponent<Props> = (props) => {
id,
properties,
fromNodeId,
additionalPortChildProps,
PortChildComponent,
onDragStart: onDragStartInternal,
onDragEnd: onDragEndInternal,
} = props;
Expand Down Expand Up @@ -296,7 +302,20 @@ const BasePort: React.FunctionComponent<Props> = (props) => {
style={style}
rx={settings.canvas.ports.radius}
ry={settings.canvas.ports.radius}
/>
>
{
(portChildProps: PortChildProps) => {
const basePortChildProps: BasePortChildProps = {
...portChildProps,
...additionalPortChildProps,
};

return <PortChildComponent
{...basePortChildProps}
/>;
}
}
</Port>
);
};

Expand Down
106 changes: 106 additions & 0 deletions src/components/ports/BasePortChild.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { css } from '@emotion/react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react';
import {
useRecoilState,
useSetRecoilState,
} from 'recoil';
import { absoluteTooltipState } from '../../states/absoluteTooltipState';
import { edgesSelector } from '../../states/edgesState';
import BaseEdgeData from '../../types/BaseEdgeData';
import BasePortChildProps from '../../types/BasePortChildProps';
import { translateXYToCanvasPosition } from '../../utils/canvas';

/**
* Each <BasePort> component contains on <BasePortChild> component provided through the "PortChildComponent" property.
*
* The BasePortChild is a <foreignObject> which displays additional content, such as:
* - A warning when an entry port is not reachable.
* - A warning when an output port is not connected.
*/
const BasePortChild: React.FunctionComponent<BasePortChildProps> = (props) => {
const {
isNodeReachable,
port,
isDragging,
x,
y,
} = props;
const setTooltip = useSetRecoilState(absoluteTooltipState);
const [edges, setEdges] = useRecoilState(edgesSelector);
const links: BaseEdgeData[] | undefined = edges?.filter((edge: BaseEdgeData) => port?.side === 'WEST' ? edge?.toPort === port?.id : edge?.fromPort === port?.id);
const shouldDisplayUnreachableWarning = port?.side === 'WEST' && !isNodeReachable;
const shouldDisplayUnlinkedWarning = port?.side === 'EAST' && !links?.length && !isDragging;
const hasContentToDisplay = shouldDisplayUnreachableWarning || shouldDisplayUnlinkedWarning;

let warning: string;

if (shouldDisplayUnreachableWarning) {
warning = `This node is not reachable because there are no edge connected to its entry port.`;
} else if (shouldDisplayUnlinkedWarning) {
warning = `This port is not linked to any other node.`;
}

// Don't render content if there are no content to display
if (!hasContentToDisplay) {
return null;
}

// Move the content a bit to the left for the West port, and to the right for the right port
const newX = x + (port?.side === 'WEST' ? -40 : 20);
const newY = y - 10;

return (
<foreignObject
// Content width/height will be limited by the width of the foreignObject
width={30}
height={30}
x={newX}
y={newY}
css={css`
position: absolute;
color: black;
z-index: 1;
.port-content {
position: fixed;
cursor: help;
pointer-events: auto;
}
.svg-inline--fa {
color: orange;
}
`}
>
<div
className={'port-content'}
onMouseEnter={(event: React.MouseEvent<HTMLDivElement, MouseEvent>): void => {
const [x, y] = translateXYToCanvasPosition(event.clientX, event.clientY);

// Displays a tooltip in absolute position based on the X/Y position of the targeted element
setTooltip({
isDisplayed: true,
text: warning,
x,
y,
});
}}
onMouseLeave={(event: React.MouseEvent<HTMLDivElement, MouseEvent>): void => {
// Hides the tooltip
setTooltip({
isDisplayed: false,
});
}}
>
<FontAwesomeIcon
icon={['fas', 'exclamation-triangle']}
/>
</div>


</foreignObject>
);
};

export default BasePortChild;
12 changes: 12 additions & 0 deletions src/states/absoluteTooltipState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { atom } from 'recoil';
import { AbsoluteTooltipContent } from '../types/AbsoluteTooltipContent';

/**
* Used to know the state of the tooltip currently being displayed.
*
* The tooltip will be displayed in absolute position.
*/
export const absoluteTooltipState = atom<AbsoluteTooltipContent | undefined>({
key: 'absoluteTooltipState',
default: undefined,
});
6 changes: 6 additions & 0 deletions src/types/AbsoluteTooltipContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type AbsoluteTooltipContent = {
isDisplayed: boolean;
text?: string;
x?: number;
y?: number;
}
19 changes: 19 additions & 0 deletions src/types/BasePortChildProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { PortChildProps } from 'reaflow';
import BaseNodeData from './BaseNodeData';

/**
* Additional properties that will be passed down to the PortChildComponent component.
*/
export type AdditionalPortChildProps = {
fromNode: BaseNodeData;
isNodeReachable: boolean;
}

/**
* Base port child properties provided to any instance of the PortChildComponent component.
*
* It inherits from Reaflow "PortChildProps" and add other custom properties on top of them.
*/
export type BasePortChildProps = PortChildProps & AdditionalPortChildProps;

export default BasePortChildProps;
Loading

1 comment on commit 1a7d418

@vercel
Copy link

@vercel vercel bot commented on 1a7d418 Mar 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.