Remove the source handle for leaf nodes (#10057)
- Do not render a source handle for the leaf nodes - Upgrade the `@xyflow/react` library | Before | After | |--------|--------| |  |  | ## Other options considered React Flow exposes a hook to get the connections of the current node. I tried to use this hook – which makes things way simpler – but I couldn't find a way to make it work in Storybook. I had two options: 1. Set up React Flow to render the nodes properly, 2. Mock the hook in Storybook. The first option was hard to achieve as the `<Reactflow />` component renders a whole flow, and it doesn't play well with the idea of rendering a single node in a story. The second option seemed overkill as mocking modules with Storybook is not straightforward. See https://storybook.js.org/docs/writing-stories/mocking-data-and-modules/mocking-modules. I chose to keep the initial version of my code, written before I spot a function simplifying the code. We can give it a look another time.
This commit is contained in:
committed by
GitHub
parent
30e4fdbd06
commit
3cc66fe712
+2
@@ -21,6 +21,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
expect(result.nodes[0]).toMatchObject({
|
||||
data: {
|
||||
nodeType: 'trigger',
|
||||
isLeafNode: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -87,6 +88,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: step.name,
|
||||
isLeafNode: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+94
-30
@@ -1,10 +1,20 @@
|
||||
import { getUuidV4Mock } from '~/testing/utils/getUuidV4Mock';
|
||||
import { getWorkflowVersionDiagram } from '../getWorkflowVersionDiagram';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: getUuidV4Mock(),
|
||||
}));
|
||||
|
||||
describe('getWorkflowVersionDiagram', () => {
|
||||
it('returns an empty diagram if the provided workflow version', () => {
|
||||
const result = getWorkflowVersionDiagram(undefined);
|
||||
|
||||
expect(result).toEqual({ nodes: [], edges: [] });
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"edges": [],
|
||||
"nodes": [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns a diagram with an empty-trigger node if the provided workflow version has no trigger', () => {
|
||||
@@ -20,17 +30,25 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
workflowId: '',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
nodes: [
|
||||
{
|
||||
data: {},
|
||||
id: 'trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
type: 'empty-trigger',
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"edges": [],
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"isLeafNode": false,
|
||||
"nodeType": "empty-trigger",
|
||||
},
|
||||
"id": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"type": "empty-trigger",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns a diagram with only a trigger node if the provided workflow version has no steps', () => {
|
||||
@@ -50,21 +68,27 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
workflowId: '',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
nodes: [
|
||||
{
|
||||
data: {
|
||||
name: 'Record is created',
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
icon: 'IconPlus',
|
||||
},
|
||||
id: 'trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"edges": [],
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"icon": "IconPlus",
|
||||
"isLeafNode": false,
|
||||
"name": "Record is created",
|
||||
"nodeType": "trigger",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns the diagram for the last version', () => {
|
||||
@@ -103,8 +127,48 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
workflowId: '',
|
||||
});
|
||||
|
||||
// Corresponds to the trigger + 1 step
|
||||
expect(result.nodes).toHaveLength(2);
|
||||
expect(result.edges).toHaveLength(1);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"edges": [
|
||||
{
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-0",
|
||||
"markerEnd": "arrow-rounded",
|
||||
"selectable": false,
|
||||
"source": "trigger",
|
||||
"target": "step-1",
|
||||
},
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"icon": "IconPlus",
|
||||
"isLeafNode": false,
|
||||
"name": "Company created",
|
||||
"nodeType": "trigger",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"isLeafNode": false,
|
||||
"name": "",
|
||||
"nodeType": "action",
|
||||
},
|
||||
"id": "step-1",
|
||||
"position": {
|
||||
"x": 150,
|
||||
"y": 100,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
|
||||
import { markLeafNodes } from '../markLeafNodes';
|
||||
|
||||
describe('markLeafNodes', () => {
|
||||
const createTrigger = (): WorkflowTrigger => ({
|
||||
name: 'Company created',
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
});
|
||||
|
||||
const createStep = (id: string): WorkflowStep => ({
|
||||
id,
|
||||
name: `Step ${id}`,
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
});
|
||||
|
||||
it('handles empty workflow with only trigger', () => {
|
||||
const trigger = createTrigger();
|
||||
const steps: WorkflowStep[] = [];
|
||||
|
||||
const diagram = generateWorkflowDiagram({ trigger, steps });
|
||||
const diagramWithLeafNodes = markLeafNodes(diagram);
|
||||
|
||||
expect(diagramWithLeafNodes.nodes).toHaveLength(1);
|
||||
expect(diagramWithLeafNodes.nodes[0].data.isLeafNode).toBe(true);
|
||||
});
|
||||
|
||||
it('handles workflow with single step', () => {
|
||||
const trigger = createTrigger();
|
||||
const steps = [createStep('step1')];
|
||||
|
||||
const diagram = generateWorkflowDiagram({ trigger, steps });
|
||||
const diagramWithLeafNodes = markLeafNodes(diagram);
|
||||
|
||||
expect(diagramWithLeafNodes.nodes).toHaveLength(2);
|
||||
expect(diagramWithLeafNodes.nodes[0].data.isLeafNode).toBe(false);
|
||||
expect(diagramWithLeafNodes.nodes[1].data.isLeafNode).toBe(true);
|
||||
});
|
||||
|
||||
it('handles workflow with two steps', () => {
|
||||
const trigger = createTrigger();
|
||||
const steps = [createStep('step1'), createStep('step2')];
|
||||
|
||||
const diagram = generateWorkflowDiagram({ trigger, steps });
|
||||
const diagramWithLeafNodes = markLeafNodes(diagram);
|
||||
|
||||
expect(diagramWithLeafNodes.nodes).toHaveLength(3);
|
||||
expect(diagramWithLeafNodes.nodes[0].data.isLeafNode).toBe(false);
|
||||
expect(diagramWithLeafNodes.nodes[1].data.isLeafNode).toBe(false);
|
||||
expect(diagramWithLeafNodes.nodes[2].data.isLeafNode).toBe(true);
|
||||
});
|
||||
});
|
||||
+66
-23
@@ -5,7 +5,12 @@ it('Preserves the properties defined in the previous version but not in the next
|
||||
const previousDiagram: WorkflowDiagram = {
|
||||
nodes: [
|
||||
{
|
||||
data: { nodeType: 'action', name: '', actionType: 'CODE' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
name: '',
|
||||
actionType: 'CODE',
|
||||
isLeafNode: true,
|
||||
},
|
||||
id: '1',
|
||||
position: { x: 0, y: 0 },
|
||||
selected: true,
|
||||
@@ -16,7 +21,12 @@ it('Preserves the properties defined in the previous version but not in the next
|
||||
const nextDiagram: WorkflowDiagram = {
|
||||
nodes: [
|
||||
{
|
||||
data: { nodeType: 'action', name: '', actionType: 'CODE' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
name: '',
|
||||
actionType: 'CODE',
|
||||
isLeafNode: true,
|
||||
},
|
||||
id: '1',
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
@@ -24,24 +34,40 @@ it('Preserves the properties defined in the previous version but not in the next
|
||||
edges: [],
|
||||
};
|
||||
|
||||
expect(mergeWorkflowDiagrams(previousDiagram, nextDiagram)).toEqual({
|
||||
nodes: [
|
||||
{
|
||||
data: { nodeType: 'action', name: '', actionType: 'CODE' },
|
||||
id: '1',
|
||||
position: { x: 0, y: 0 },
|
||||
selected: true,
|
||||
expect(mergeWorkflowDiagrams(previousDiagram, nextDiagram))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"edges": [],
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"isLeafNode": true,
|
||||
"name": "",
|
||||
"nodeType": "action",
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
});
|
||||
"id": "1",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"selected": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('Replaces duplicated properties with the next value', () => {
|
||||
const previousDiagram: WorkflowDiagram = {
|
||||
nodes: [
|
||||
{
|
||||
data: { nodeType: 'action', name: '', actionType: 'CODE' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
name: '',
|
||||
actionType: 'CODE',
|
||||
isLeafNode: true,
|
||||
},
|
||||
id: '1',
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
@@ -51,7 +77,12 @@ it('Replaces duplicated properties with the next value', () => {
|
||||
const nextDiagram: WorkflowDiagram = {
|
||||
nodes: [
|
||||
{
|
||||
data: { nodeType: 'action', name: '2', actionType: 'CODE' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
name: '2',
|
||||
actionType: 'CODE',
|
||||
isLeafNode: false,
|
||||
},
|
||||
id: '1',
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
@@ -59,14 +90,26 @@ it('Replaces duplicated properties with the next value', () => {
|
||||
edges: [],
|
||||
};
|
||||
|
||||
expect(mergeWorkflowDiagrams(previousDiagram, nextDiagram)).toEqual({
|
||||
nodes: [
|
||||
{
|
||||
data: { nodeType: 'action', name: '2', actionType: 'CODE' },
|
||||
id: '1',
|
||||
position: { x: 0, y: 0 },
|
||||
expect(mergeWorkflowDiagrams(previousDiagram, nextDiagram))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"edges": [],
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"isLeafNode": false,
|
||||
"name": "2",
|
||||
"nodeType": "action",
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
});
|
||||
"id": "1",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"selected": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
+10
-3
@@ -5,7 +5,9 @@ import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workf
|
||||
import {
|
||||
WorkflowDiagram,
|
||||
WorkflowDiagramEdge,
|
||||
WorkflowDiagramEmptyTriggerNodeData,
|
||||
WorkflowDiagramNode,
|
||||
WorkflowDiagramStepNodeData,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes';
|
||||
|
||||
@@ -38,7 +40,8 @@ export const generateWorkflowDiagram = ({
|
||||
nodeType: 'action',
|
||||
actionType: step.type,
|
||||
name: step.name,
|
||||
},
|
||||
isLeafNode: false,
|
||||
} satisfies WorkflowDiagramStepNodeData,
|
||||
position: {
|
||||
x: xPos,
|
||||
y: yPos,
|
||||
@@ -102,7 +105,8 @@ export const generateWorkflowDiagram = ({
|
||||
triggerType: trigger.type,
|
||||
name: isDefined(trigger.name) ? trigger.name : triggerDefaultLabel,
|
||||
icon: triggerIcon,
|
||||
},
|
||||
isLeafNode: false,
|
||||
} satisfies WorkflowDiagramStepNodeData,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -112,7 +116,10 @@ export const generateWorkflowDiagram = ({
|
||||
nodes.push({
|
||||
id: triggerNodeId,
|
||||
type: 'empty-trigger',
|
||||
data: {} as any,
|
||||
data: {
|
||||
nodeType: 'empty-trigger',
|
||||
isLeafNode: false,
|
||||
} satisfies WorkflowDiagramEmptyTriggerNodeData,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { CREATE_STEP_STEP_ID } from '@/workflow/workflow-diagram/constants/CreateStepStepId';
|
||||
import {
|
||||
WorkflowDiagramCreateStepNodeData,
|
||||
WorkflowDiagramNode,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
|
||||
export const isCreateStepNode = (
|
||||
node: WorkflowDiagramNode,
|
||||
): node is WorkflowDiagramNode & {
|
||||
data: WorkflowDiagramCreateStepNodeData;
|
||||
} => {
|
||||
return (
|
||||
node.type === CREATE_STEP_STEP_ID && node.data.nodeType === 'create-step'
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
WorkflowDiagram,
|
||||
WorkflowDiagramNode,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { isCreateStepNode } from '@/workflow/workflow-diagram/utils/isCreateStepNode';
|
||||
|
||||
export const markLeafNodes = ({
|
||||
nodes,
|
||||
edges,
|
||||
}: WorkflowDiagram): WorkflowDiagram => {
|
||||
const sourceNodeIds = new Set(edges.map((edge) => edge.source));
|
||||
|
||||
const updatedNodes = nodes.map((node) => {
|
||||
if (isCreateStepNode(node)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
isLeafNode: !sourceNodeIds.has(node.id),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
nodes: updatedNodes as WorkflowDiagramNode[],
|
||||
edges,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user