mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-27 16:39:06 -05:00
feat: updates anchor connector schema
This commit is contained in:
@@ -2,7 +2,12 @@ import React, { useMemo } from 'react';
|
||||
import { useTheme, Box } from '@mui/material';
|
||||
import { Connector as ConnectorI } from 'src/types';
|
||||
import { UNPROJECTED_TILE_SIZE } from 'src/config';
|
||||
import { getAnchorPosition, CoordsUtils, getColorVariant } from 'src/utils';
|
||||
import {
|
||||
getAnchorPosition,
|
||||
CoordsUtils,
|
||||
getColorVariant,
|
||||
getAllAnchors
|
||||
} from 'src/utils';
|
||||
import { Circle } from 'src/components/Circle/Circle';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
@@ -27,6 +32,9 @@ export const Connector = ({ connector }: Props) => {
|
||||
const nodes = useSceneStore((state) => {
|
||||
return state.nodes;
|
||||
});
|
||||
const connectors = useSceneStore((state) => {
|
||||
return state.connectors;
|
||||
});
|
||||
|
||||
const drawOffset = useMemo(() => {
|
||||
return {
|
||||
@@ -45,14 +53,24 @@ export const Connector = ({ connector }: Props) => {
|
||||
|
||||
const anchorPositions = useMemo(() => {
|
||||
return connector.anchors.map((anchor) => {
|
||||
const position = getAnchorPosition({ anchor, nodes });
|
||||
const position = getAnchorPosition({
|
||||
anchor,
|
||||
nodes,
|
||||
allAnchors: getAllAnchors(connectors)
|
||||
});
|
||||
|
||||
return {
|
||||
x: (connector.path.rectangle.from.x - position.x) * unprojectedTileSize,
|
||||
y: (connector.path.rectangle.from.y - position.y) * unprojectedTileSize
|
||||
};
|
||||
});
|
||||
}, [connector.path.rectangle, connector.anchors, nodes, unprojectedTileSize]);
|
||||
}, [
|
||||
connector.path.rectangle,
|
||||
connector.anchors,
|
||||
nodes,
|
||||
connectors,
|
||||
unprojectedTileSize
|
||||
]);
|
||||
|
||||
const connectorWidthPx = useMemo(() => {
|
||||
return (unprojectedTileSize / 100) * connector.width;
|
||||
|
||||
@@ -51,16 +51,16 @@ export const initialScene: InitialScene = {
|
||||
connectors: [
|
||||
{
|
||||
id: 'connector1',
|
||||
anchors: [{ nodeId: 'server' }, { nodeId: 'database' }]
|
||||
anchors: [{ ref: { node: 'server' } }, { ref: { node: 'database' } }]
|
||||
},
|
||||
{
|
||||
id: 'connector2',
|
||||
style: ConnectorStyleEnum.DOTTED,
|
||||
width: 10,
|
||||
anchors: [
|
||||
{ nodeId: 'server' },
|
||||
{ tile: { x: -1, y: 2 } },
|
||||
{ nodeId: 'client' }
|
||||
{ ref: { node: 'server' } },
|
||||
{ ref: { tile: { x: -1, y: 2 } } },
|
||||
{ ref: { node: 'client' } }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
getBoundingBoxSize,
|
||||
sortByPosition,
|
||||
clamp,
|
||||
getAnchorPosition
|
||||
getAnchorPosition,
|
||||
getAllAnchors
|
||||
} from 'src/utils';
|
||||
import { useGetTilePosition } from 'src/hooks/useGetTilePosition';
|
||||
import { useScroll } from 'src/hooks/useScroll';
|
||||
@@ -43,7 +44,11 @@ export const useDiagramUtils = () => {
|
||||
return [
|
||||
...acc,
|
||||
...item.anchors.map((anchor) => {
|
||||
return getAnchorPosition({ anchor, nodes: scene.nodes });
|
||||
return getAnchorPosition({
|
||||
anchor,
|
||||
nodes: scene.nodes,
|
||||
allAnchors: getAllAnchors(scene.connectors)
|
||||
});
|
||||
})
|
||||
];
|
||||
case 'RECTANGLE':
|
||||
@@ -60,7 +65,7 @@ export const useDiagramUtils = () => {
|
||||
|
||||
return corners;
|
||||
},
|
||||
[scene.nodes]
|
||||
[scene.nodes, scene.connectors]
|
||||
);
|
||||
|
||||
const getUnprojectedBounds = useCallback((): Size & Coords => {
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
connectorToConnectorInput,
|
||||
getConnectorPath,
|
||||
hasMovedTile,
|
||||
setWindowCursor
|
||||
setWindowCursor,
|
||||
getAllAnchors
|
||||
} from 'src/utils';
|
||||
import { ModeActions } from 'src/types';
|
||||
|
||||
@@ -37,13 +38,16 @@ export const Connector: ModeActions = {
|
||||
if (!draftState.connector) return;
|
||||
|
||||
draftState.connector.anchors[1] = {
|
||||
type: 'NODE',
|
||||
id: itemAtTile.id
|
||||
ref: {
|
||||
type: 'NODE',
|
||||
id: itemAtTile.id
|
||||
}
|
||||
};
|
||||
|
||||
draftState.connector.path = getConnectorPath({
|
||||
anchors: draftState.connector.anchors,
|
||||
nodes: scene.nodes
|
||||
nodes: scene.nodes,
|
||||
allAnchors: getAllAnchors(scene.connectors)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,13 +57,16 @@ export const Connector: ModeActions = {
|
||||
if (!draftState.connector) return;
|
||||
|
||||
draftState.connector.anchors[1] = {
|
||||
type: 'TILE',
|
||||
coords: uiState.mouse.position.tile
|
||||
ref: {
|
||||
type: 'TILE',
|
||||
coords: uiState.mouse.position.tile
|
||||
}
|
||||
};
|
||||
|
||||
draftState.connector.path = getConnectorPath({
|
||||
anchors: draftState.connector.anchors,
|
||||
nodes: scene.nodes
|
||||
nodes: scene.nodes,
|
||||
allAnchors: getAllAnchors(scene.connectors)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,9 +86,13 @@ export const Connector: ModeActions = {
|
||||
draftState.connector = connectorInputToConnector(
|
||||
{
|
||||
id: generateId(),
|
||||
anchors: [{ nodeId: itemAtTile.id }, { nodeId: itemAtTile.id }]
|
||||
anchors: [
|
||||
{ ref: { node: itemAtTile.id } },
|
||||
{ ref: { node: itemAtTile.id } }
|
||||
]
|
||||
},
|
||||
scene.nodes
|
||||
scene.nodes,
|
||||
getAllAnchors(scene.connectors)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -92,11 +103,12 @@ export const Connector: ModeActions = {
|
||||
{
|
||||
id: generateId(),
|
||||
anchors: [
|
||||
{ tile: uiState.mouse.position.tile },
|
||||
{ tile: uiState.mouse.position.tile }
|
||||
{ ref: { tile: uiState.mouse.position.tile } },
|
||||
{ ref: { tile: uiState.mouse.position.tile } }
|
||||
]
|
||||
},
|
||||
scene.nodes
|
||||
scene.nodes,
|
||||
getAllAnchors(scene.connectors)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
textBoxInputToTextBox,
|
||||
sceneInputToScene,
|
||||
nodeInputToNode,
|
||||
getTextWidth
|
||||
getTextWidth,
|
||||
getAllAnchors
|
||||
} from 'src/utils';
|
||||
|
||||
export const initialScene: Scene = {
|
||||
@@ -65,13 +66,14 @@ const initialState = () => {
|
||||
|
||||
draftState.connectors.forEach((connector, i) => {
|
||||
const needsUpdate = connector.anchors.find((anchor) => {
|
||||
return anchor.type === 'NODE' && anchor.id === id;
|
||||
return anchor.ref.type === 'NODE' && anchor.ref.id === id;
|
||||
});
|
||||
|
||||
if (needsUpdate) {
|
||||
draftState.connectors[i].path = getConnectorPath({
|
||||
anchors: connector.anchors,
|
||||
nodes: draftState.nodes
|
||||
nodes: draftState.nodes,
|
||||
allAnchors: getAllAnchors(draftState.connectors)
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -89,7 +91,7 @@ const initialState = () => {
|
||||
draftState.connectors = draftState.connectors.filter(
|
||||
(connector) => {
|
||||
return !connector.anchors.find((anchor) => {
|
||||
return anchor.type === 'NODE' && anchor.id === id;
|
||||
return anchor.ref.type === 'NODE' && anchor.ref.id === id;
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -101,7 +103,11 @@ const initialState = () => {
|
||||
createConnector: (connector) => {
|
||||
const newScene = produce(get(), (draftState) => {
|
||||
draftState.connectors.push(
|
||||
connectorInputToConnector(connector, draftState.nodes)
|
||||
connectorInputToConnector(
|
||||
connector,
|
||||
draftState.nodes,
|
||||
getAllAnchors(draftState.connectors)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
4
src/tests/fixtures/scene.ts
vendored
4
src/tests/fixtures/scene.ts
vendored
@@ -45,11 +45,11 @@ export const scene: SceneInput = {
|
||||
connectors: [
|
||||
{
|
||||
id: 'connector1',
|
||||
anchors: [{ nodeId: 'node1' }, { nodeId: 'node2' }]
|
||||
anchors: [{ ref: { node: 'node1' } }, { ref: { node: 'node2' } }]
|
||||
},
|
||||
{
|
||||
id: 'connector2',
|
||||
anchors: [{ nodeId: 'node2' }, { nodeId: 'node3' }]
|
||||
anchors: [{ ref: { node: 'node2' } }, { ref: { node: 'node3' } }]
|
||||
}
|
||||
],
|
||||
textBoxes: [],
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface Node {
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
export type ConnectorAnchor =
|
||||
export type ConnectorAnchorRef =
|
||||
| {
|
||||
type: 'NODE';
|
||||
id: string;
|
||||
@@ -42,8 +42,17 @@ export type ConnectorAnchor =
|
||||
| {
|
||||
type: 'TILE';
|
||||
coords: Coords;
|
||||
}
|
||||
| {
|
||||
type: 'ANCHOR';
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type ConnectorAnchor = {
|
||||
id?: string;
|
||||
ref: ConnectorAnchorRef;
|
||||
};
|
||||
|
||||
export interface Connector {
|
||||
type: SceneItemTypeEnum.CONNECTOR;
|
||||
id: string;
|
||||
|
||||
@@ -14,8 +14,7 @@ import {
|
||||
TextBox,
|
||||
Rectangle,
|
||||
ConnectorAnchorInput,
|
||||
ConnectorAnchor,
|
||||
Coords
|
||||
ConnectorAnchor
|
||||
} from 'src/types';
|
||||
import {
|
||||
NODE_DEFAULTS,
|
||||
@@ -63,22 +62,43 @@ export const rectangleInputToRectangle = (
|
||||
const connectorAnchorInputToConnectorAnchor = (
|
||||
anchor: ConnectorAnchorInput
|
||||
): ConnectorAnchor => {
|
||||
if (anchor.nodeId) {
|
||||
if (anchor.ref.node) {
|
||||
return {
|
||||
type: 'NODE',
|
||||
id: anchor.nodeId
|
||||
id: anchor.id,
|
||||
ref: {
|
||||
type: 'NODE',
|
||||
id: anchor.ref.node
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'TILE',
|
||||
coords: anchor.tile as Coords
|
||||
};
|
||||
if (anchor.ref.tile) {
|
||||
return {
|
||||
id: anchor.id,
|
||||
ref: {
|
||||
type: 'TILE',
|
||||
coords: anchor.ref.tile
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (anchor.ref.anchor) {
|
||||
return {
|
||||
id: anchor.id,
|
||||
ref: {
|
||||
type: 'ANCHOR',
|
||||
id: anchor.ref.anchor
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Could not render connector anchor');
|
||||
};
|
||||
|
||||
export const connectorInputToConnector = (
|
||||
connectorInput: ConnectorInput,
|
||||
nodes: Node[]
|
||||
nodes: Node[],
|
||||
allAnchors: ConnectorAnchor[]
|
||||
): Connector => {
|
||||
const anchors = connectorInput.anchors
|
||||
.map((anchor) => {
|
||||
@@ -95,7 +115,7 @@ export const connectorInputToConnector = (
|
||||
width: connectorInput.width ?? CONNECTOR_DEFAULTS.width,
|
||||
style: connectorInput.style ?? CONNECTOR_DEFAULTS.style,
|
||||
anchors,
|
||||
path: getConnectorPath({ anchors, nodes })
|
||||
path: getConnectorPath({ anchors, nodes, allAnchors })
|
||||
};
|
||||
};
|
||||
|
||||
@@ -130,6 +150,17 @@ export const textBoxToTextBoxInput = (textBox: TextBox): TextBoxInput => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getAllAnchorsFromInput = (connectors: ConnectorInput[]) => {
|
||||
const allAnchors = connectors.reduce((acc, connectorInput) => {
|
||||
const convertedAnchors = connectorInput.anchors.map((anchor) => {
|
||||
return connectorAnchorInputToConnectorAnchor(anchor);
|
||||
});
|
||||
return [...acc, ...convertedAnchors];
|
||||
}, [] as ConnectorAnchor[]);
|
||||
|
||||
return allAnchors;
|
||||
};
|
||||
|
||||
export const sceneInputToScene = (sceneInput: SceneInput): Scene => {
|
||||
const icons = sceneInput.icons.map((icon) => {
|
||||
return iconInputToIcon(icon);
|
||||
@@ -143,8 +174,10 @@ export const sceneInputToScene = (sceneInput: SceneInput): Scene => {
|
||||
return rectangleInputToRectangle(rectangleInput);
|
||||
});
|
||||
|
||||
const allAnchors = getAllAnchorsFromInput(sceneInput.connectors);
|
||||
|
||||
const connectors = sceneInput.connectors.map((connectorInput) => {
|
||||
return connectorInputToConnector(connectorInput, nodes);
|
||||
return connectorInputToConnector(connectorInput, nodes, allAnchors);
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -178,14 +211,21 @@ export const nodeToNodeInput = (node: Node): NodeInput => {
|
||||
export const connectorAnchorToConnectorAnchorInput = (
|
||||
anchor: ConnectorAnchor
|
||||
): ConnectorAnchorInput | null => {
|
||||
switch (anchor.type) {
|
||||
switch (anchor.ref.type) {
|
||||
case 'NODE':
|
||||
return {
|
||||
nodeId: anchor.id
|
||||
id: anchor.id,
|
||||
ref: { node: anchor.ref.id }
|
||||
};
|
||||
case 'TILE':
|
||||
return {
|
||||
tile: anchor.coords
|
||||
id: anchor.id,
|
||||
ref: { tile: anchor.ref.coords }
|
||||
};
|
||||
case 'ANCHOR':
|
||||
return {
|
||||
id: anchor.id,
|
||||
ref: { anchor: anchor.ref.id }
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Coords,
|
||||
TileOriginEnum,
|
||||
Node,
|
||||
Connector,
|
||||
Size,
|
||||
Scroll,
|
||||
Mouse,
|
||||
@@ -327,21 +328,39 @@ export function getItemById<T extends { id: string }>(
|
||||
return { item: items[index], index };
|
||||
}
|
||||
|
||||
export const getAllAnchors = (connectors: Connector[]) => {
|
||||
return connectors.reduce((acc, connector) => {
|
||||
return [...acc, ...connector.anchors];
|
||||
}, [] as ConnectorAnchor[]);
|
||||
};
|
||||
|
||||
interface GetAnchorPositions {
|
||||
anchor: ConnectorAnchor;
|
||||
nodes: Node[];
|
||||
allAnchors: ConnectorAnchor[];
|
||||
}
|
||||
|
||||
export const getAnchorPosition = ({
|
||||
anchor,
|
||||
nodes
|
||||
nodes,
|
||||
allAnchors
|
||||
}: GetAnchorPositions): Coords => {
|
||||
if (anchor.type === 'NODE') {
|
||||
const { item: node } = getItemById(nodes, anchor.id);
|
||||
if (anchor.ref.type === 'NODE') {
|
||||
const { item: node } = getItemById(nodes, anchor.ref.id);
|
||||
return node.position;
|
||||
}
|
||||
|
||||
return anchor.coords;
|
||||
if (anchor.ref.type === 'ANCHOR') {
|
||||
const anchorsWithIds = allAnchors.filter((_anchor) => {
|
||||
return _anchor.id !== undefined;
|
||||
}) as Required<ConnectorAnchor>[];
|
||||
|
||||
const nextAnchor = getItemById(anchorsWithIds, anchor.ref.id);
|
||||
|
||||
return getAnchorPosition({ anchor: nextAnchor.item, nodes, allAnchors });
|
||||
}
|
||||
|
||||
return anchor.ref.coords;
|
||||
};
|
||||
|
||||
interface NormalisePositionFromOrigin {
|
||||
@@ -359,11 +378,13 @@ export const normalisePositionFromOrigin = ({
|
||||
interface GetConnectorPath {
|
||||
anchors: ConnectorAnchor[];
|
||||
nodes: Node[];
|
||||
allAnchors: ConnectorAnchor[];
|
||||
}
|
||||
|
||||
export const getConnectorPath = ({
|
||||
anchors,
|
||||
nodes
|
||||
nodes,
|
||||
allAnchors
|
||||
}: GetConnectorPath): {
|
||||
tiles: Coords[];
|
||||
rectangle: Rect;
|
||||
@@ -374,7 +395,7 @@ export const getConnectorPath = ({
|
||||
);
|
||||
|
||||
const anchorPositions = anchors.map((anchor) => {
|
||||
return getAnchorPosition({ anchor, nodes });
|
||||
return getAnchorPosition({ anchor, nodes, allAnchors });
|
||||
});
|
||||
|
||||
const searchArea = getBoundingBox(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// TODO: Split into individual files
|
||||
import { z } from 'zod';
|
||||
import { INITIAL_SCENE } from '../config';
|
||||
import {
|
||||
iconInput,
|
||||
nodeInput,
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
rectangleInput,
|
||||
textBoxInput
|
||||
} from './sceneItems';
|
||||
import { findInvalidConnector, findInvalidNode } from './utils';
|
||||
import { ensureValidConnectors, ensureValidNodes } from './utils';
|
||||
|
||||
export const sceneInput = z
|
||||
.object({
|
||||
@@ -17,30 +18,16 @@ export const sceneInput = z
|
||||
textBoxes: z.array(textBoxInput),
|
||||
rectangles: z.array(rectangleInput)
|
||||
})
|
||||
.superRefine((scene, ctx) => {
|
||||
const icons = scene.icons ?? [];
|
||||
const nodes = scene.nodes ?? [];
|
||||
const connectors = scene.connectors ?? [];
|
||||
.superRefine((_scene, ctx) => {
|
||||
const scene = { ...INITIAL_SCENE, ..._scene };
|
||||
|
||||
const invalidNode = findInvalidNode(nodes, icons);
|
||||
|
||||
if (invalidNode) {
|
||||
ctx.addIssue({
|
||||
try {
|
||||
ensureValidNodes(scene);
|
||||
ensureValidConnectors(scene);
|
||||
} catch (e) {
|
||||
return ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['nodes', invalidNode.id],
|
||||
message: 'Invalid node found in scene'
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const invalidConnector = findInvalidConnector(connectors, nodes);
|
||||
|
||||
if (invalidConnector) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['connectors', invalidConnector.id],
|
||||
message: 'Invalid connector found in scene'
|
||||
message: e.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -23,18 +23,24 @@ export const nodeInput = z.object({
|
||||
});
|
||||
|
||||
export const connectorAnchorInput = z
|
||||
// TODO: See if we can use `z.discriminatedUnion` here. See https://github.com/colinhacks/zod#unions
|
||||
.object({
|
||||
nodeId: z.string(),
|
||||
tile: coords
|
||||
id: z.string().optional(),
|
||||
ref: z
|
||||
.object({
|
||||
node: z.string(),
|
||||
anchor: z.string(),
|
||||
tile: coords
|
||||
})
|
||||
.partial()
|
||||
})
|
||||
.partial()
|
||||
.superRefine((data, ctx) => {
|
||||
if (!data.nodeId && !data.tile) {
|
||||
const definedRefs = Object.keys(data.ref);
|
||||
|
||||
if (definedRefs.length !== 1) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['connectorAnchor'],
|
||||
message: 'Connector anchor needs either a nodeId or tile coords.'
|
||||
message:
|
||||
'Connector anchor should be associated with only either a node, another anchor or a tile.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { NodeInput, ConnectorInput } from 'src/types/inputs';
|
||||
import { produce } from 'immer';
|
||||
import { sceneInput } from '../scene';
|
||||
import { findInvalidNode, findInvalidConnector } from '../utils';
|
||||
import { scene } from '../../tests/fixtures/scene';
|
||||
import { scene as sceneFixture } from '../../tests/fixtures/scene';
|
||||
|
||||
describe('scene validation works correctly', () => {
|
||||
test('scene fixture is valid', () => {
|
||||
const result = sceneInput.safeParse(scene);
|
||||
const result = sceneInput.safeParse(sceneFixture);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -14,33 +13,55 @@ describe('scene validation works correctly', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('finds invalid nodes in scene', () => {
|
||||
const { icons } = scene;
|
||||
test('node with invalid iconId fails validation', () => {
|
||||
const invalidNode = {
|
||||
id: 'invalidNode',
|
||||
iconId: 'doesntExist',
|
||||
position: { x: -1, y: -1 }
|
||||
};
|
||||
const nodes: NodeInput[] = [...scene.nodes, invalidNode];
|
||||
const scene = produce(sceneFixture, (draft) => {
|
||||
draft.nodes.push(invalidNode);
|
||||
});
|
||||
|
||||
const result = findInvalidNode(nodes, icons);
|
||||
const result = sceneInput.safeParse(scene);
|
||||
|
||||
expect(result).toEqual(invalidNode);
|
||||
expect(result.success).toBe(false);
|
||||
if (result.success === false) {
|
||||
expect(result.error.errors[0].message).toContain('invalid icon');
|
||||
}
|
||||
});
|
||||
|
||||
test('finds invalid connectors in scene', () => {
|
||||
const { nodes } = scene;
|
||||
test('connector with anchor that references an invalid node fails validation', () => {
|
||||
const invalidConnector = {
|
||||
id: 'invalidConnector',
|
||||
anchors: [{ nodeId: 'node1' }, { nodeId: 'invalidNode' }]
|
||||
anchors: [{ ref: { node: 'node1' } }, { ref: { node: 'invalidNode' } }]
|
||||
};
|
||||
const connectors: ConnectorInput[] = [
|
||||
...scene.connectors,
|
||||
invalidConnector
|
||||
];
|
||||
const scene = produce(sceneFixture, (draft) => {
|
||||
draft.connectors.push(invalidConnector);
|
||||
});
|
||||
|
||||
const result = findInvalidConnector(connectors, nodes);
|
||||
const result = sceneInput.safeParse(scene);
|
||||
|
||||
expect(result).toEqual(invalidConnector);
|
||||
expect(result.success).toBe(false);
|
||||
if (result.success === false) {
|
||||
expect(result.error.errors[0].message).toContain('node does not exist');
|
||||
}
|
||||
});
|
||||
|
||||
test('connector with anchor that references an invalid anchor fails validation', () => {
|
||||
const invalidConnector = {
|
||||
id: 'invalidConnector',
|
||||
anchors: [{ ref: { anchor: 'invalidAnchor' } }]
|
||||
};
|
||||
const scene = produce(sceneFixture, (draft) => {
|
||||
draft.connectors.push(invalidConnector);
|
||||
});
|
||||
|
||||
const result = sceneInput.safeParse(scene);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
if (result.success === false) {
|
||||
expect(result.error.errors[0].message).toContain('anchor does not exist');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,32 +1,78 @@
|
||||
import { ConnectorInput, NodeInput, IconInput } from 'src/types/inputs';
|
||||
import {
|
||||
ConnectorInput,
|
||||
NodeInput,
|
||||
ConnectorAnchorInput,
|
||||
ConnectorAnchor,
|
||||
SceneInput
|
||||
} from 'src/types';
|
||||
import { getAllAnchorsFromInput, getItemById } from 'src/utils';
|
||||
|
||||
export const findInvalidNode = (nodes: NodeInput[], icons: IconInput[]) => {
|
||||
return nodes.find((node) => {
|
||||
const validIcon = icons.find((icon) => {
|
||||
return node.iconId === icon.id;
|
||||
});
|
||||
return !validIcon;
|
||||
export const ensureValidNode = (node: NodeInput, scene: SceneInput) => {
|
||||
try {
|
||||
getItemById(scene.icons, node.iconId);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Found invalid node [id: "${node.id}"] referencing an invalid icon [id: "${node.iconId}"]`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const ensureValidNodes = (scene: SceneInput) => {
|
||||
scene.nodes.forEach((node) => {
|
||||
ensureValidNode(node, scene);
|
||||
});
|
||||
};
|
||||
|
||||
export const findInvalidConnector = (
|
||||
connectors: ConnectorInput[],
|
||||
export const ensureValidConnectorAnchor = (
|
||||
anchor: ConnectorAnchorInput,
|
||||
allAnchors: ConnectorAnchor[],
|
||||
nodes: NodeInput[]
|
||||
) => {
|
||||
return connectors.find((con) => {
|
||||
const invalidAnchor = con.anchors.find((anchor) => {
|
||||
if (
|
||||
anchor.nodeId &&
|
||||
!nodes.find((node) => {
|
||||
return node.id === anchor.nodeId;
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
if (anchor.ref.node) {
|
||||
const nodeExists = nodes.find((node) => {
|
||||
return node.id === anchor.ref.node;
|
||||
});
|
||||
|
||||
return Boolean(invalidAnchor);
|
||||
if (!nodeExists) {
|
||||
throw new Error(
|
||||
`Anchor [id: "${anchor.id}"] references a node [id: "${anchor.ref.node}"], but that node does not exist`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (anchor.ref.anchor) {
|
||||
const nextAnchor = allAnchors.find((_anchor) => {
|
||||
return _anchor.id === anchor.ref.anchor;
|
||||
});
|
||||
|
||||
if (!nextAnchor) {
|
||||
throw new Error(
|
||||
`Anchor [id: "${anchor.id}"] references another anchor [id: "${anchor.ref.anchor}"], but that anchor does not exist`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ensureValidConnector = (
|
||||
connector: ConnectorInput,
|
||||
allAnchors: ConnectorAnchor[],
|
||||
nodes: NodeInput[]
|
||||
) => {
|
||||
connector.anchors.forEach((anchor) => {
|
||||
try {
|
||||
ensureValidConnectorAnchor(anchor, allAnchors, nodes);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Found invalid connector [id: "${connector.id}"]: ${e.message}`
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const ensureValidConnectors = (scene: SceneInput) => {
|
||||
const allAnchors = getAllAnchorsFromInput(scene.connectors);
|
||||
|
||||
scene.connectors.forEach((con) => {
|
||||
ensureValidConnector(con, allAnchors, scene.nodes);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"module": "es6",
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"useUnknownInCatchVariables": false
|
||||
},
|
||||
"exclude": ["node_modules", "./dist", "./docs"],
|
||||
"include": [
|
||||
|
||||
Reference in New Issue
Block a user