diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/CardComponents.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/CardComponents.tsx index 436ad06a2..a0cde2c4f 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/CardComponents.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/CardComponents.tsx @@ -8,8 +8,7 @@ import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableE import { FieldsCard } from '@/object-record/record-show/components/FieldsCard'; import { CardType } from '@/object-record/record-show/types/CardType'; import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer'; -import { WorkflowRunOutputVisualizer } from '@/workflow/components/WorkflowRunOutputVisualizer'; -import { WorkflowRunVersionVisualizer } from '@/workflow/components/WorkflowRunVersionVisualizer'; +import { WorkflowRunVisualizer } from '@/workflow/components/WorkflowRunVisualizer'; import { WorkflowVersionVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizer'; import { WorkflowVersionVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect'; import { WorkflowVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVisualizer'; @@ -94,10 +93,6 @@ export const CardComponents: Record = { ), [CardType.WorkflowRunCard]: ({ targetableObject }) => ( - - ), - - [CardType.WorkflowRunOutputCard]: ({ targetableObject }) => ( - + ), }; diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts index 7d0ae3ec1..ad610af24 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts @@ -13,7 +13,6 @@ import { IconCalendarEvent, IconMail, IconNotes, - IconPrinter, IconSettings, } from 'twenty-ui'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -180,20 +179,6 @@ export const useRecordShowContainerTabs = ( }, [CoreObjectNameSingular.WorkflowRun]: { tabs: { - workflowRunOutput: { - title: 'Output', - position: 0, - Icon: IconPrinter, - cards: [{ type: CardType.WorkflowRunOutputCard }], - hide: { - ifMobile: false, - ifDesktop: false, - ifInRightDrawer: false, - ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled], - ifRequiredObjectsInactive: [], - ifRelationsMissing: [], - }, - }, workflowRunFlow: { title: 'Flow', position: 0, diff --git a/packages/twenty-front/src/modules/object-record/record-show/types/CardType.ts b/packages/twenty-front/src/modules/object-record/record-show/types/CardType.ts index 6a805d0af..2045ba3d6 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/types/CardType.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/types/CardType.ts @@ -9,6 +9,5 @@ export enum CardType { WorkflowCard = 'WorkflowCard', WorkflowVersionCard = 'WorkflowVersionCard', WorkflowRunCard = 'WorkflowRunCard', - WorkflowRunOutputCard = 'WorkflowRunOutputCard', RichTextCard = 'RichTextCard', } diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowRunVersionVisualizer.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowRunVersionVisualizer.tsx deleted file mode 100644 index dd6ac30f4..000000000 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowRunVersionVisualizer.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun'; -import { WorkflowVersionVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizer'; -import { WorkflowVersionVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect'; -import { isDefined } from 'twenty-shared'; - -export const WorkflowRunVersionVisualizer = ({ - workflowRunId, -}: { - workflowRunId: string; -}) => { - const workflowRun = useWorkflowRun({ - workflowRunId, - }); - if (!isDefined(workflowRun)) { - return null; - } - - return ( - <> - - - - - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowRunOutputVisualizer.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowRunVisualizer.tsx similarity index 60% rename from packages/twenty-front/src/modules/workflow/components/WorkflowRunOutputVisualizer.tsx rename to packages/twenty-front/src/modules/workflow/components/WorkflowRunVisualizer.tsx index 836d3bea1..fa65542e0 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowRunOutputVisualizer.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowRunVisualizer.tsx @@ -1,13 +1,13 @@ +import { WorkflowRunVisualizerContent } from '@/workflow/components/WorkflowRunVisualizerContent'; import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun'; import styled from '@emotion/styled'; import { isDefined } from 'twenty-shared'; -import { CodeEditor } from 'twenty-ui'; const StyledSourceCodeContainer = styled.div` - margin: ${({ theme }) => theme.spacing(4)}; + height: 100%; `; -export const WorkflowRunOutputVisualizer = ({ +export const WorkflowRunVisualizer = ({ workflowRunId, }: { workflowRunId: string; @@ -19,11 +19,7 @@ export const WorkflowRunOutputVisualizer = ({ return ( - + ); }; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowRunVisualizerContent.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowRunVisualizerContent.tsx new file mode 100644 index 000000000..903234264 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowRunVisualizerContent.tsx @@ -0,0 +1,27 @@ +import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion'; +import { WorkflowRun } from '@/workflow/types/Workflow'; +import { WorkflowDiagramCanvasReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly'; +import { WorkflowRunVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect'; +import { isDefined } from 'twenty-shared'; + +export const WorkflowRunVisualizerContent = ({ + workflowRun, +}: { + workflowRun: WorkflowRun; +}) => { + const workflowVersion = useWorkflowVersion(workflowRun.workflowVersionId); + if (!isDefined(workflowVersion)) { + return null; + } + + return ( + <> + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index 3daa533d2..a6f22cd36 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -206,7 +206,7 @@ export type WorkflowRun = { __typename: 'WorkflowRun'; id: string; workflowVersionId: string; - output: WorkflowRunOutput; + output: WorkflowRunOutput | null; }; export type Workflow = { diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx index af59403b9..5fbcad3a4 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx @@ -1,4 +1,4 @@ -import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; +import { WorkflowVersionStatus } from '@/workflow/types/Workflow'; import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase'; import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect'; import { WorkflowDiagramCreateStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepNode'; @@ -8,14 +8,14 @@ import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/com import { ReactFlowProvider } from '@xyflow/react'; export const WorkflowDiagramCanvasEditable = ({ - workflowWithCurrentVersion, + versionStatus, }: { - workflowWithCurrentVersion: WorkflowWithCurrentVersion; + versionStatus: WorkflowVersionStatus; }) => { return ( { return ( { + switch (runStatus) { + case 'success': + return 'success'; + case 'failure': + return 'failure'; + case 'running': + return 'default'; + case 'not-executed': + return 'not-executed'; + default: + return 'default'; + } +}; export const WorkflowDiagramStepNodeReadonly = ({ data, @@ -10,7 +30,7 @@ export const WorkflowDiagramStepNodeReadonly = ({ return ( } isLeafNode={data.isLeafNode} diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx new file mode 100644 index 000000000..6e8b96b5b --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx @@ -0,0 +1,49 @@ +import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion'; +import { workflowVersionIdState } from '@/workflow/states/workflowVersionIdState'; +import { WorkflowRun } from '@/workflow/types/Workflow'; +import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflowDiagramState'; +import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram'; +import { useEffect } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared'; + +export const WorkflowRunVisualizerEffect = ({ + workflowVersionId, + workflowRun, +}: { + workflowVersionId: string; + workflowRun: WorkflowRun; +}) => { + const workflowVersion = useWorkflowVersion(workflowVersionId); + + const setWorkflowVersionId = useSetRecoilState(workflowVersionIdState); + const setWorkflowDiagram = useSetRecoilState(workflowDiagramState); + + useEffect(() => { + setWorkflowVersionId(workflowVersionId); + }, [setWorkflowVersionId, workflowVersionId]); + + useEffect(() => { + if ( + !( + isDefined(workflowVersion) && + isDefined(workflowVersion.trigger) && + isDefined(workflowVersion.steps) + ) + ) { + setWorkflowDiagram(undefined); + + return; + } + + const nextWorkflowDiagram = generateWorkflowRunDiagram({ + trigger: workflowVersion.trigger, + steps: workflowVersion.steps, + output: workflowRun.output, + }); + + setWorkflowDiagram(nextWorkflowDiagram); + }, [setWorkflowDiagram, workflowRun.output, workflowVersion]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizer.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizer.tsx index c62c8a235..9d909a6eb 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizer.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizer.tsx @@ -11,6 +11,6 @@ export const WorkflowVersionVisualizer = ({ const workflowVersion = useWorkflowVersion(workflowVersionId); return isDefined(workflowVersion) ? ( - + ) : null; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVisualizer.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVisualizer.tsx index b2485cdda..901c22ed5 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVisualizer.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVisualizer.tsx @@ -15,7 +15,7 @@ export const WorkflowVisualizer = ({ workflowId }: { workflowId: string }) => { {isDefined(workflowWithCurrentVersion) ? ( ) : null} diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/FirstNodePosition.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/FirstNodePosition.ts new file mode 100644 index 000000000..58e05f885 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/FirstNodePosition.ts @@ -0,0 +1,4 @@ +export const FIRST_NODE_POSITION = { + x: 0, + y: 0, +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/VerticalDistanceBetweenTwoNodes.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/VerticalDistanceBetweenTwoNodes.ts new file mode 100644 index 000000000..6be63040e --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/VerticalDistanceBetweenTwoNodes.ts @@ -0,0 +1 @@ +export const VERTICAL_DISTANCE_BETWEEN_TWO_NODES = 150; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowDiagramEmptyTriggerNodeDefinition.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowDiagramEmptyTriggerNodeDefinition.ts new file mode 100644 index 000000000..bd0d179eb --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowDiagramEmptyTriggerNodeDefinition.ts @@ -0,0 +1,16 @@ +import { WorkflowDiagramEmptyTriggerNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId'; +import { Node } from '@xyflow/react'; + +export const WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION = { + id: TRIGGER_STEP_ID, + type: 'empty-trigger', + data: { + nodeType: 'empty-trigger', + isLeafNode: false, + } satisfies WorkflowDiagramEmptyTriggerNodeData, + position: { + x: 0, + y: 0, + }, +} satisfies Node; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowDiagramSuccessEdgeType.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowDiagramSuccessEdgeType.ts new file mode 100644 index 000000000..0ba1f2b45 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowDiagramSuccessEdgeType.ts @@ -0,0 +1,4 @@ +import { WorkflowDiagramEdgeType } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; + +export const WORKFLOW_DIAGRAM_SUCCESS_EDGE_TYPE = + 'success' satisfies WorkflowDiagramEdgeType; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration.ts index a6d23019a..0d6788a7e 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration.ts @@ -1,8 +1,10 @@ import { EDGE_GREEN_CIRCLE_MARKED_ID } from '@/workflow/workflow-diagram/constants/EdgeGreenCircleMarkedId'; import { EDGE_GREEN_ROUNDED_ARROW_MARKER_ID } from '@/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerId'; +import { WORKFLOW_DIAGRAM_SUCCESS_EDGE_TYPE } from '@/workflow/workflow-diagram/constants/WorkflowDiagramSuccessEdgeType'; import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; export const WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION = { + type: WORKFLOW_DIAGRAM_SUCCESS_EDGE_TYPE, markerStart: EDGE_GREEN_CIRCLE_MARKED_ID, markerEnd: EDGE_GREEN_ROUNDED_ARROW_MARKER_ID, deletable: false, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagram.ts index 7c3dd37d3..7828bc5b8 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagram.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagram.ts @@ -7,23 +7,39 @@ import { Edge, Node } from '@xyflow/react'; export type WorkflowDiagramNode = Node; export type WorkflowDiagramEdge = Edge; +export type WorkflowRunDiagramNode = Node; +export type WorkflowRunDiagramEdge = Edge; + +export type WorkflowRunDiagram = { + nodes: Array; + edges: Array; +}; + export type WorkflowDiagram = { nodes: Array; edges: Array; }; +export type WorkflowDiagramRunStatus = + | 'running' + | 'success' + | 'failure' + | 'not-executed'; + export type WorkflowDiagramStepNodeData = | { nodeType: 'trigger'; triggerType: WorkflowTriggerType; name: string; icon?: string; + runStatus?: WorkflowDiagramRunStatus; isLeafNode: boolean; } | { nodeType: 'action'; actionType: WorkflowActionType; name: string; + runStatus?: WorkflowDiagramRunStatus; isLeafNode: boolean; }; @@ -43,6 +59,11 @@ export type WorkflowDiagramNodeData = | WorkflowDiagramCreateStepNodeData | WorkflowDiagramEmptyTriggerNodeData; +export type WorkflowRunDiagramNodeData = Exclude< + WorkflowDiagramStepNodeData, + 'runStatus' +> & { runStatus: WorkflowDiagramRunStatus }; + export type WorkflowDiagramNodeType = | 'default' | 'empty-trigger' diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowRunDiagram.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowRunDiagram.test.ts new file mode 100644 index 000000000..739a6b6b1 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowRunDiagram.test.ts @@ -0,0 +1,955 @@ +import { + WorkflowRunOutput, + WorkflowStep, + WorkflowTrigger, +} from '@/workflow/types/Workflow'; +import { getUuidV4Mock } from '~/testing/utils/getUuidV4Mock'; +import { generateWorkflowRunDiagram } from '../generateWorkflowRunDiagram'; + +jest.mock('uuid', () => ({ + v4: getUuidV4Mock(), +})); + +describe('generateWorkflowRunDiagram', () => { + it('marks node as failed when not at least one attempt is in output', () => { + const trigger: WorkflowTrigger = { + name: 'Company created', + type: 'DATABASE_EVENT', + settings: { + eventName: 'company.created', + outputSchema: {}, + }, + }; + const steps: WorkflowStep[] = [ + { + id: 'step1', + name: 'Step 1', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + { + id: 'step2', + name: 'Step 2', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + { + id: 'step3', + name: 'Step 3', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + ]; + const output: WorkflowRunOutput = { + steps: { + step1: { + id: 'step1', + name: 'Step 1', + outputs: [], + type: 'CODE', + }, + }, + }; + + const result = generateWorkflowRunDiagram({ trigger, steps, output }); + + expect(result).toMatchInlineSnapshot(` +{ + "edges": [ + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-0", + "markerEnd": "workflow-edge-green-arrow-rounded", + "markerStart": "workflow-edge-green-circle", + "selectable": false, + "source": "trigger", + "target": "step1", + "type": "success", + }, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-1", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", + "selectable": false, + "source": "step1", + "target": "step2", + }, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-2", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", + "selectable": false, + "source": "step2", + "target": "step3", + }, + ], + "nodes": [ + { + "data": { + "icon": "IconPlaylistAdd", + "isLeafNode": false, + "name": "Company created", + "nodeType": "trigger", + "runStatus": "success", + "triggerType": "DATABASE_EVENT", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 1", + "nodeType": "action", + "runStatus": "failure", + }, + "id": "step1", + "position": { + "x": 0, + "y": 0, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 2", + "nodeType": "action", + "runStatus": "not-executed", + }, + "id": "step2", + "position": { + "x": 0, + "y": 150, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 3", + "nodeType": "action", + "runStatus": "not-executed", + }, + "id": "step3", + "position": { + "x": 0, + "y": 300, + }, + }, + ], +} +`); + }); + + it('marks node as failed when the last attempt failed', () => { + const trigger: WorkflowTrigger = { + name: 'Company created', + type: 'DATABASE_EVENT', + settings: { + eventName: 'company.created', + outputSchema: {}, + }, + }; + const steps: WorkflowStep[] = [ + { + id: 'step1', + name: 'Step 1', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + { + id: 'step2', + name: 'Step 2', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + { + id: 'step3', + name: 'Step 3', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + ]; + const output: WorkflowRunOutput = { + steps: { + step1: { + id: 'step1', + name: 'Step 1', + outputs: [ + { + attemptCount: 1, + result: undefined, + error: '', + }, + ], + type: 'CODE', + }, + }, + }; + + const result = generateWorkflowRunDiagram({ trigger, steps, output }); + + expect(result).toMatchInlineSnapshot(` +{ + "edges": [ + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-3", + "markerEnd": "workflow-edge-green-arrow-rounded", + "markerStart": "workflow-edge-green-circle", + "selectable": false, + "source": "trigger", + "target": "step1", + "type": "success", + }, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-4", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", + "selectable": false, + "source": "step1", + "target": "step2", + }, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-5", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", + "selectable": false, + "source": "step2", + "target": "step3", + }, + ], + "nodes": [ + { + "data": { + "icon": "IconPlaylistAdd", + "isLeafNode": false, + "name": "Company created", + "nodeType": "trigger", + "runStatus": "success", + "triggerType": "DATABASE_EVENT", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 1", + "nodeType": "action", + "runStatus": "failure", + }, + "id": "step1", + "position": { + "x": 0, + "y": 0, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 2", + "nodeType": "action", + "runStatus": "not-executed", + }, + "id": "step2", + "position": { + "x": 0, + "y": 150, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 3", + "nodeType": "action", + "runStatus": "not-executed", + }, + "id": "step3", + "position": { + "x": 0, + "y": 300, + }, + }, + ], +} +`); + }); + + it('marks all nodes as successful when each node has an output', () => { + const trigger: WorkflowTrigger = { + name: 'Company created', + type: 'DATABASE_EVENT', + settings: { + eventName: 'company.created', + outputSchema: {}, + }, + }; + const steps: WorkflowStep[] = [ + { + id: 'step1', + name: 'Step 1', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + { + id: 'step2', + name: 'Step 2', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + { + id: 'step3', + name: 'Step 3', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + ]; + const output: WorkflowRunOutput = { + steps: { + step1: { + id: 'step1', + name: 'Step 1', + outputs: [ + { + attemptCount: 1, + result: {}, + error: undefined, + }, + ], + type: 'CODE', + }, + step2: { + id: 'step2', + name: 'Step 2', + outputs: [ + { + attemptCount: 1, + result: {}, + error: undefined, + }, + ], + type: 'CODE', + }, + step3: { + id: 'step3', + name: 'Step 3', + outputs: [ + { + attemptCount: 1, + result: {}, + error: undefined, + }, + ], + type: 'CODE', + }, + }, + }; + + const result = generateWorkflowRunDiagram({ trigger, steps, output }); + + expect(result).toMatchInlineSnapshot(` +{ + "edges": [ + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-6", + "markerEnd": "workflow-edge-green-arrow-rounded", + "markerStart": "workflow-edge-green-circle", + "selectable": false, + "source": "trigger", + "target": "step1", + "type": "success", + }, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-7", + "markerEnd": "workflow-edge-green-arrow-rounded", + "markerStart": "workflow-edge-green-circle", + "selectable": false, + "source": "step1", + "target": "step2", + "type": "success", + }, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-8", + "markerEnd": "workflow-edge-green-arrow-rounded", + "markerStart": "workflow-edge-green-circle", + "selectable": false, + "source": "step2", + "target": "step3", + "type": "success", + }, + ], + "nodes": [ + { + "data": { + "icon": "IconPlaylistAdd", + "isLeafNode": false, + "name": "Company created", + "nodeType": "trigger", + "runStatus": "success", + "triggerType": "DATABASE_EVENT", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 1", + "nodeType": "action", + "runStatus": "success", + }, + "id": "step1", + "position": { + "x": 0, + "y": 0, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 2", + "nodeType": "action", + "runStatus": "success", + }, + "id": "step2", + "position": { + "x": 0, + "y": 150, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 3", + "nodeType": "action", + "runStatus": "success", + }, + "id": "step3", + "position": { + "x": 0, + "y": 300, + }, + }, + ], +} +`); + }); + + it('marks node as running and all other ones as not-executed when no output is available at all', () => { + const trigger: WorkflowTrigger = { + name: 'Company created', + type: 'DATABASE_EVENT', + settings: { + eventName: 'company.created', + outputSchema: {}, + }, + }; + const steps: WorkflowStep[] = [ + { + id: 'step1', + name: 'Step 1', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + { + id: 'step2', + name: 'Step 2', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + { + id: 'step3', + name: 'Step 3', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + ]; + const output = null; + + const result = generateWorkflowRunDiagram({ trigger, steps, output }); + + expect(result).toMatchInlineSnapshot(` +{ + "edges": [ + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-9", + "markerEnd": "workflow-edge-green-arrow-rounded", + "markerStart": "workflow-edge-green-circle", + "selectable": false, + "source": "trigger", + "target": "step1", + "type": "success", + }, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-10", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", + "selectable": false, + "source": "step1", + "target": "step2", + }, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-11", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", + "selectable": false, + "source": "step2", + "target": "step3", + }, + ], + "nodes": [ + { + "data": { + "icon": "IconPlaylistAdd", + "isLeafNode": false, + "name": "Company created", + "nodeType": "trigger", + "runStatus": "success", + "triggerType": "DATABASE_EVENT", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 1", + "nodeType": "action", + "runStatus": "running", + }, + "id": "step1", + "position": { + "x": 0, + "y": 0, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 2", + "nodeType": "action", + "runStatus": "not-executed", + }, + "id": "step2", + "position": { + "x": 0, + "y": 150, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 3", + "nodeType": "action", + "runStatus": "not-executed", + }, + "id": "step3", + "position": { + "x": 0, + "y": 300, + }, + }, + ], +} +`); + }); + + it("marks node as running and all other ones as not-executed when a node doesn't have an attached output", () => { + const trigger: WorkflowTrigger = { + name: 'Company created', + type: 'DATABASE_EVENT', + settings: { + eventName: 'company.created', + outputSchema: {}, + }, + }; + const steps: WorkflowStep[] = [ + { + id: 'step1', + name: 'Step 1', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + { + id: 'step2', + name: 'Step 2', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + { + id: 'step3', + name: 'Step 3', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + { + id: 'step4', + name: 'Step 4', + type: 'CODE', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + }, + }, + ]; + const output: WorkflowRunOutput = { + steps: { + step1: { + id: 'step1', + name: 'Step 1', + outputs: [ + { + attemptCount: 1, + result: {}, + error: undefined, + }, + ], + type: 'CODE', + }, + }, + }; + + const result = generateWorkflowRunDiagram({ trigger, steps, output }); + + expect(result).toMatchInlineSnapshot(` +{ + "edges": [ + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-12", + "markerEnd": "workflow-edge-green-arrow-rounded", + "markerStart": "workflow-edge-green-circle", + "selectable": false, + "source": "trigger", + "target": "step1", + "type": "success", + }, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-13", + "markerEnd": "workflow-edge-green-arrow-rounded", + "markerStart": "workflow-edge-green-circle", + "selectable": false, + "source": "step1", + "target": "step2", + "type": "success", + }, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-14", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", + "selectable": false, + "source": "step2", + "target": "step3", + }, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-15", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", + "selectable": false, + "source": "step3", + "target": "step4", + }, + ], + "nodes": [ + { + "data": { + "icon": "IconPlaylistAdd", + "isLeafNode": false, + "name": "Company created", + "nodeType": "trigger", + "runStatus": "success", + "triggerType": "DATABASE_EVENT", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 1", + "nodeType": "action", + "runStatus": "success", + }, + "id": "step1", + "position": { + "x": 0, + "y": 0, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 2", + "nodeType": "action", + "runStatus": "running", + }, + "id": "step2", + "position": { + "x": 0, + "y": 150, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 3", + "nodeType": "action", + "runStatus": "not-executed", + }, + "id": "step3", + "position": { + "x": 0, + "y": 300, + }, + }, + { + "data": { + "actionType": "CODE", + "isLeafNode": false, + "name": "Step 4", + "nodeType": "action", + "runStatus": "not-executed", + }, + "id": "step4", + "position": { + "x": 0, + "y": 450, + }, + }, + ], +} +`); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionDiagram.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionDiagram.test.ts index 46ed6a8ad..d49ecc104 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionDiagram.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionDiagram.test.ts @@ -164,8 +164,8 @@ describe('getWorkflowVersionDiagram', () => { }, "id": "step-1", "position": { - "x": 150, - "y": 100, + "x": 0, + "y": 150, }, }, ], diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts index 450249786..9219e90a8 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts @@ -1,18 +1,17 @@ import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow'; -import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; -import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName'; +import { FIRST_NODE_POSITION } from '@/workflow/workflow-diagram/constants/FirstNodePosition'; +import { VERTICAL_DISTANCE_BETWEEN_TWO_NODES } from '@/workflow/workflow-diagram/constants/VerticalDistanceBetweenTwoNodes'; +import { WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEmptyTriggerNodeDefinition'; import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeDefaultConfiguration'; import { WorkflowDiagram, WorkflowDiagramEdge, - WorkflowDiagramEmptyTriggerNodeData, WorkflowDiagramNode, WorkflowDiagramStepNodeData, } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes'; +import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode'; import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId'; -import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon'; import { isDefined } from 'twenty-shared'; import { v4 } from 'uuid'; @@ -26,12 +25,28 @@ export const generateWorkflowDiagram = ({ const nodes: Array = []; const edges: Array = []; - const processNode = ( - step: WorkflowStep, - parentNodeId: string, - xPos: number, - yPos: number, - ) => { + if (isDefined(trigger)) { + nodes.push(getWorkflowDiagramTriggerNode({ trigger })); + } else { + nodes.push(WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION); + } + + const processNode = ({ + stepIndex, + parentNodeId, + xPos, + yPos, + }: { + stepIndex: number; + parentNodeId: string; + xPos: number; + yPos: number; + }) => { + const step = steps.at(stepIndex); + if (!isDefined(step)) { + return; + } + const nodeId = step.id; nodes.push({ @@ -44,7 +59,7 @@ export const generateWorkflowDiagram = ({ } satisfies WorkflowDiagramStepNodeData, position: { x: xPos, - y: yPos, + y: yPos + VERTICAL_DISTANCE_BETWEEN_TWO_NODES, }, }); @@ -55,91 +70,20 @@ export const generateWorkflowDiagram = ({ target: nodeId, }); - return nodeId; + processNode({ + stepIndex: stepIndex + 1, + parentNodeId: nodeId, + xPos, + yPos: yPos + VERTICAL_DISTANCE_BETWEEN_TWO_NODES, + }); }; - const triggerNodeId = TRIGGER_STEP_ID; - - if (isDefined(trigger)) { - let triggerDefaultLabel: string; - let triggerIcon: string | undefined; - - switch (trigger.type) { - case 'MANUAL': { - triggerDefaultLabel = 'Manual Trigger'; - triggerIcon = getTriggerIcon({ - type: 'MANUAL', - }); - - break; - } - case 'CRON': { - triggerDefaultLabel = 'On a Schedule'; - triggerIcon = getTriggerIcon({ - type: 'CRON', - }); - - break; - } - case 'DATABASE_EVENT': { - const triggerEvent = splitWorkflowTriggerEventName( - trigger.settings.eventName, - ); - - triggerDefaultLabel = - DATABASE_TRIGGER_TYPES.find( - (item) => item.event === triggerEvent.event, - )?.defaultLabel ?? ''; - - triggerIcon = getTriggerIcon({ - type: 'DATABASE_EVENT', - eventName: triggerEvent.event, - }); - - break; - } - default: { - return assertUnreachable( - trigger, - `Expected the trigger "${JSON.stringify(trigger)}" to be supported.`, - ); - } - } - - nodes.push({ - id: triggerNodeId, - data: { - nodeType: 'trigger', - triggerType: trigger.type, - name: isDefined(trigger.name) ? trigger.name : triggerDefaultLabel, - icon: triggerIcon, - isLeafNode: false, - } satisfies WorkflowDiagramStepNodeData, - position: { - x: 0, - y: 0, - }, - }); - } else { - nodes.push({ - id: triggerNodeId, - type: 'empty-trigger', - data: { - nodeType: 'empty-trigger', - isLeafNode: false, - } satisfies WorkflowDiagramEmptyTriggerNodeData, - position: { - x: 0, - y: 0, - }, - }); - } - - let lastStepId = triggerNodeId; - - for (const step of steps) { - lastStepId = processNode(step, lastStepId, 150, 100); - } + processNode({ + stepIndex: 0, + parentNodeId: TRIGGER_STEP_ID, + xPos: FIRST_NODE_POSITION.x, + yPos: FIRST_NODE_POSITION.y, + }); return { nodes, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowRunDiagram.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowRunDiagram.ts new file mode 100644 index 000000000..ef30d2c0a --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowRunDiagram.ts @@ -0,0 +1,141 @@ +import { + WorkflowRunOutput, + WorkflowStep, + WorkflowTrigger, +} from '@/workflow/types/Workflow'; +import { FIRST_NODE_POSITION } from '@/workflow/workflow-diagram/constants/FirstNodePosition'; +import { VERTICAL_DISTANCE_BETWEEN_TWO_NODES } from '@/workflow/workflow-diagram/constants/VerticalDistanceBetweenTwoNodes'; +import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeDefaultConfiguration'; +import { WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration'; +import { + WorkflowDiagramRunStatus, + WorkflowRunDiagram, + WorkflowRunDiagramEdge, + WorkflowRunDiagramNode, +} from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode'; +import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId'; +import { isDefined } from 'twenty-shared'; +import { v4 } from 'uuid'; + +export const generateWorkflowRunDiagram = ({ + trigger, + steps, + output, +}: { + trigger: WorkflowTrigger; + steps: Array; + output: WorkflowRunOutput | null; +}): WorkflowRunDiagram => { + const triggerBase = getWorkflowDiagramTriggerNode({ trigger }); + + const nodes: Array = [ + { + ...triggerBase, + data: { + ...triggerBase.data, + runStatus: 'success', + }, + }, + ]; + const edges: Array = []; + + const processNode = ({ + stepIndex, + parentNodeId, + parentRunStatus, + xPos, + yPos, + skippedExecution, + }: { + stepIndex: number; + parentNodeId: string; + parentRunStatus: WorkflowDiagramRunStatus; + xPos: number; + yPos: number; + skippedExecution: boolean; + }) => { + const step = steps.at(stepIndex); + if (!isDefined(step)) { + return; + } + + const nodeId = step.id; + + if (parentRunStatus === 'success') { + edges.push({ + ...WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION, + id: v4(), + source: parentNodeId, + target: nodeId, + }); + } else { + edges.push({ + ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION, + id: v4(), + source: parentNodeId, + target: nodeId, + }); + } + + const runResult = output?.steps[nodeId]; + + let runStatus: WorkflowDiagramRunStatus; + if (skippedExecution) { + runStatus = 'not-executed'; + } else if (!isDefined(runResult)) { + runStatus = 'running'; + } else { + const lastAttempt = runResult.outputs.at(-1); + + if (!isDefined(lastAttempt)) { + // Should never happen. Should we throw instead? + runStatus = 'failure'; + } else if (isDefined(lastAttempt.error)) { + runStatus = 'failure'; + } else { + runStatus = 'success'; + } + } + + nodes.push({ + id: nodeId, + data: { + nodeType: 'action', + actionType: step.type, + name: step.name, + isLeafNode: false, + runStatus, + }, + position: { + x: xPos, + y: yPos, + }, + }); + + processNode({ + stepIndex: stepIndex + 1, + parentNodeId: nodeId, + parentRunStatus: runStatus, + xPos, + yPos: yPos + VERTICAL_DISTANCE_BETWEEN_TWO_NODES, + skippedExecution: skippedExecution + ? true + : runStatus === 'failure' || runStatus === 'running', + }); + }; + + processNode({ + stepIndex: 0, + parentNodeId: TRIGGER_STEP_ID, + parentRunStatus: 'success', + xPos: FIRST_NODE_POSITION.x, + yPos: FIRST_NODE_POSITION.y, + skippedExecution: false, + }); + + return { + nodes, + edges, + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode.ts new file mode 100644 index 000000000..103449dc5 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode.ts @@ -0,0 +1,74 @@ +import { WorkflowTrigger } from '@/workflow/types/Workflow'; +import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; +import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName'; +import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes'; +import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId'; +import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon'; +import { Node } from '@xyflow/react'; +import { isDefined } from 'twenty-shared'; + +export const getWorkflowDiagramTriggerNode = ({ + trigger, +}: { + trigger: WorkflowTrigger; +}): Node => { + let triggerDefaultLabel: string; + let triggerIcon: string | undefined; + + switch (trigger.type) { + case 'MANUAL': { + triggerDefaultLabel = 'Manual Trigger'; + triggerIcon = getTriggerIcon({ + type: 'MANUAL', + }); + + break; + } + case 'CRON': { + triggerDefaultLabel = 'On a Schedule'; + triggerIcon = getTriggerIcon({ + type: 'CRON', + }); + + break; + } + case 'DATABASE_EVENT': { + const triggerEvent = splitWorkflowTriggerEventName( + trigger.settings.eventName, + ); + + triggerDefaultLabel = + DATABASE_TRIGGER_TYPES.find((item) => item.event === triggerEvent.event) + ?.defaultLabel ?? ''; + + triggerIcon = getTriggerIcon({ + type: 'DATABASE_EVENT', + eventName: triggerEvent.event, + }); + + break; + } + default: { + return assertUnreachable( + trigger, + `Expected the trigger "${JSON.stringify(trigger)}" to be supported.`, + ); + } + } + + return { + id: TRIGGER_STEP_ID, + data: { + nodeType: 'trigger', + triggerType: trigger.type, + name: isDefined(trigger.name) ? trigger.name : triggerDefaultLabel, + icon: triggerIcon, + isLeafNode: false, + } satisfies WorkflowDiagramStepNodeData, + position: { + x: 0, + y: 0, + }, + }; +};