mirror of
https://github.com/xodio/xod.git
synced 2026-03-22 00:26:54 +01:00
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:
@@ -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,
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user