feat(xod-client): draw links with incompatible pin types as broken and show tooltips with error message for any broken link

This commit is contained in:
Kirill Shumilov
2018-03-15 19:52:10 +03:00
parent 7787a4e8a6
commit 2ffeec0f40
4 changed files with 90 additions and 67 deletions

View File

@@ -2,10 +2,20 @@ import * as R from 'ramda';
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { mapIndexed } from 'xod-func-tools';
import { noop } from '../../utils/ramda';
import { PIN_RADIUS, LINK_HOTSPOT_SIZE } from '../nodeLayout';
import TooltipHOC from '../../tooltip/components/TooltipHOC';
// :: [Error] -> [ReactNode]
const renderTooltipContent = mapIndexed((err, idx) => (
<div key={idx} className="Tooltip--error">
{err.message}
</div>
));
class Link extends React.Component {
constructor(props) {
super(props);
@@ -61,33 +71,45 @@ class Link extends React.Component {
const linkEndRadius = PIN_RADIUS - 3;
return (
<g
className={cls}
id={this.elementId}
onClick={this.onClick}
style={{ pointerEvents }}
>
<line
stroke="transparent"
strokeWidth={LINK_HOTSPOT_SIZE.WIDTH}
{...coords}
/>
<line className="line" {...coords} />
<circle
className="end"
cx={coords.x1}
cy={coords.y1}
r={linkEndRadius}
fill="black"
/>
<circle
className="end"
cx={coords.x2}
cy={coords.y2}
r={linkEndRadius}
fill="black"
/>
</g>
<TooltipHOC
content={
R.isEmpty(this.props.errors)
? null
: renderTooltipContent(this.props.errors)
}
render={(onMouseOver, onMouseMove, onMouseLeave) => (
<g
className={cls}
id={this.elementId}
onClick={this.onClick}
onMouseOver={onMouseOver}
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
style={{ pointerEvents }}
>
<line
stroke="transparent"
strokeWidth={LINK_HOTSPOT_SIZE.WIDTH}
{...coords}
/>
<line className="line" {...coords} />
<circle
className="end"
cx={coords.x1}
cy={coords.y1}
r={linkEndRadius}
fill="black"
/>
<circle
className="end"
cx={coords.x2}
cy={coords.y2}
r={linkEndRadius}
fill="black"
/>
</g>
)}
/>
);
}
}
@@ -98,6 +120,7 @@ Link.propTypes = {
to: PropTypes.object.isRequired,
type: PropTypes.string.isRequired,
dead: PropTypes.bool,
errors: PropTypes.arrayOf(PropTypes.instanceOf(Error)),
isSelected: PropTypes.bool,
isGhost: PropTypes.bool,
isOverlay: PropTypes.bool,
@@ -107,6 +130,7 @@ Link.propTypes = {
Link.defaultProps = {
dead: false,
errors: [],
isSelected: false,
isGhost: false,
isOverlay: false,

View File

@@ -20,6 +20,8 @@ const LinksOverlayLayer = ({ links, selection, hidden, onClick }) => (
from={link.from}
to={link.to}
type={link.type}
dead={link.dead}
errors={link.errors}
onClick={onClick}
isSelected={isLinkSelected(selection, link.id)}
/>

View File

@@ -3,7 +3,7 @@ import { Maybe } from 'ramda-fantasy';
import { createSelector } from 'reselect';
import * as XP from 'xod-project';
import { foldMaybe, foldEither } from 'xod-func-tools';
import { foldMaybe, foldEither, explodeMaybe } from 'xod-func-tools';
import { createIndexFromPatches } from 'xod-patch-search';
import {
@@ -138,12 +138,12 @@ export const getCurrentPatch = createSelector(
(patchPath, project) => R.chain(XP.getPatchByPath(R.__, project), patchPath)
);
// :: Error -> RenderableNode -> RenderableNode
const addError = R.curry((error, node) =>
// :: Error -> RenderableNode|RenderableLink -> RenderableNode|RenderableLink
const addError = R.curry((error, renderableEntity) =>
R.compose(
R.over(R.lensProp('errors'), R.append(error)),
R.unless(R.has('errors'), R.assoc('errors', []))
)(node)
)(renderableEntity)
);
// :: Project -> RenderableNode -> RenderableNode
@@ -235,39 +235,37 @@ export const getRenderableNodes = createMemoizedSelector(
// :: State -> StrMap RenderableLink
export const getRenderableLinks = createMemoizedSelector(
[getRenderableNodes, getCurrentPatchLinks],
[R.equals, R.equals],
(nodes, links) =>
[getRenderableNodes, getCurrentPatchLinks, getCurrentPatch, getProject],
[R.equals, R.equals, R.equals, R.equals],
(nodes, links, curPatch, project) =>
R.compose(
addLinksPositioning(nodes),
R.map(link => {
const inputNodeId = XP.getLinkInputNodeId(link);
const outputNodeId = XP.getLinkOutputNodeId(link);
const inputPinKey = XP.getLinkInputPinKey(link);
const outputPinKey = XP.getLinkOutputPinKey(link);
const inputNodeIsDead = nodes[inputNodeId].dead;
const outputNodeIsDead = nodes[outputNodeId].dead;
const inputPinKeyHasDeadType = R.pathEq(
[inputNodeId, 'pins', inputPinKey, 'type'],
XP.PIN_TYPE.DEAD,
nodes
);
const outputPinKeyHasDeadType = R.pathEq(
[outputNodeId, 'pins', outputPinKey, 'type'],
XP.PIN_TYPE.DEAD,
nodes
);
return R.merge(link, {
type: nodes[outputNodeId].pins[outputPinKey].type,
dead:
inputNodeIsDead ||
outputNodeIsDead ||
inputPinKeyHasDeadType ||
outputPinKeyHasDeadType,
});
})
R.map(link =>
R.compose(
newLink => {
const outputNodeId = XP.getLinkOutputNodeId(link);
const outputPinKey = XP.getLinkOutputPinKey(link);
return R.assoc(
'type',
nodes[outputNodeId].pins[outputPinKey].type,
newLink
);
},
foldEither(
R.pipe(addError(R.__, link), R.assoc('dead', true)),
R.identity
),
R.map(R.always(link)),
XP.validateLinkPins
)(
link,
explodeMaybe(
'Imposible error: RenderableLinks will be computed only for current patch',
curPatch
),
project
)
)
)(links)
);

View File

@@ -475,15 +475,14 @@ const checkLinkPin = def(
/**
* Checks project for existence of patches and pins that used in link.
*
* @private
* @function checkPinKeys
* @function validateLinkPins
* @param {Link} link
* @param {Patch} patch
* @param {Project} project
* @returns {Either<Error|Patch>}
*/
const checkPinKeys = def(
'checkPinKeys :: Link -> Patch -> Project -> Either Error Patch',
export const validateLinkPins = def(
'validateLinkPins :: Link -> Patch -> Project -> Either Error Patch',
(link, patch, project) => {
const checkInputPin = checkLinkPin(
Link.getLinkInputNodeId,
@@ -548,7 +547,7 @@ export const validatePatchContents = def(
R.compose(
R.map(R.always(patch)),
R.sequence(Either.of),
R.map(checkPinKeys(R.__, patch, project))
R.map(validateLinkPins(R.__, patch, project))
)
),
Patch.listLinks