diff --git a/packages/twenty-e2e-testing/lib/fixtures/blank-workflow.ts b/packages/twenty-e2e-testing/lib/fixtures/blank-workflow.ts index d02e03931..9db1cc4b5 100644 --- a/packages/twenty-e2e-testing/lib/fixtures/blank-workflow.ts +++ b/packages/twenty-e2e-testing/lib/fixtures/blank-workflow.ts @@ -24,6 +24,8 @@ export class WorkflowVisualizerPage { readonly useAsDraftButton: Locator; readonly overrideDraftButton: Locator; readonly discardDraftButton: Locator; + readonly seeRunsButton: Locator; + readonly goBackInCommandMenu: Locator; #actionNames: Record = { 'create-record': 'Create Record', @@ -31,6 +33,7 @@ export class WorkflowVisualizerPage { 'delete-record': 'Delete Record', code: 'Code', 'send-email': 'Send Email', + form: 'Form', }; #createdActionNames: Record = { @@ -39,6 +42,7 @@ export class WorkflowVisualizerPage { 'delete-record': 'Delete Record', code: 'Code - Serverless Function', 'send-email': 'Send Email', + form: 'Form', }; #triggerNames: Record = { @@ -84,6 +88,10 @@ export class WorkflowVisualizerPage { this.discardDraftButton = page.getByRole('button', { name: 'Discard Draft', }); + this.seeRunsButton = page.getByRole('link', { name: 'See runs' }); + this.goBackInCommandMenu = this.commandMenu + .getByRole('button') + .and(this.commandMenu.getByTestId('command-menu-go-back-button')); } async createOneWorkflow() { diff --git a/packages/twenty-e2e-testing/lib/types/workflows.ts b/packages/twenty-e2e-testing/lib/types/workflows.ts index 6b1f4bec8..f1ddf07a4 100644 --- a/packages/twenty-e2e-testing/lib/types/workflows.ts +++ b/packages/twenty-e2e-testing/lib/types/workflows.ts @@ -9,4 +9,5 @@ export type WorkflowActionType = | 'update-record' | 'delete-record' | 'code' - | 'send-email'; + | 'send-email' + | 'form'; diff --git a/packages/twenty-e2e-testing/tests/workflow-run.spec.ts b/packages/twenty-e2e-testing/tests/workflow-run.spec.ts index 21dccb661..3473a8fc8 100644 --- a/packages/twenty-e2e-testing/tests/workflow-run.spec.ts +++ b/packages/twenty-e2e-testing/tests/workflow-run.spec.ts @@ -56,3 +56,113 @@ test('The workflow run visualizer shows the executed draft version without the l 'Create Record', ); }); + +test('Workflow Runs with a pending form step can be opened in the side panel and then in full screen', async ({ + workflowVisualizer, + page, +}) => { + await workflowVisualizer.createInitialTrigger('manual'); + + const manualTriggerAvailabilitySelect = page.getByRole('button', { + name: 'When record(s) are selected', + }); + + await manualTriggerAvailabilitySelect.click(); + + const alwaysAvailableOption = page.getByText( + 'When no record(s) are selected', + ); + + await alwaysAvailableOption.click(); + + await workflowVisualizer.closeSidePanel(); + + const { createdStepId: firstStepId } = + await workflowVisualizer.createStep('form'); + + await workflowVisualizer.closeSidePanel(); + + const launchTestButton = page.getByLabel(workflowVisualizer.workflowName); + + await launchTestButton.click(); + + const goToExecutionPageLink = page.getByRole('link', { + name: 'View execution details', + }); + + await expect(goToExecutionPageLink).toBeVisible(); + + await workflowVisualizer.seeRunsButton.click(); + + const workflowRunName = `#1 - ${workflowVisualizer.workflowName}`; + + const workflowRunNameCell = page.getByRole('cell', { name: workflowRunName }); + + await expect(workflowRunNameCell).toBeVisible(); + + const recordTableOptionsButton = page.getByText('Options'); + + await recordTableOptionsButton.click(); + + const layoutButton = page.getByText('Layout'); + + await layoutButton.click(); + + const openInButton = page.getByText('Open in'); + + await openInButton.click(); + + const openInSidePanelOption = page.getByRole('option', { + name: 'Side panel', + }); + + await openInSidePanelOption.click(); + + // 1. Exit the dropdown + await workflowRunNameCell.click(); + // 2. Actually open the workflow run in the side panel + await workflowRunNameCell.click(); + + await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText( + 'Form', + ); + + await workflowVisualizer.goBackInCommandMenu.click(); + + const workflowRunNameInCommandMenu = + workflowVisualizer.commandMenu.getByText(workflowRunName); + + await expect(workflowRunNameInCommandMenu).toBeVisible(); + + await workflowVisualizer.triggerNode.click(); + + await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText( + 'Launch manually', + ); + + await workflowVisualizer.goBackInCommandMenu.click(); + + const formStep = workflowVisualizer.getStepNode(firstStepId); + + await formStep.click(); + + await workflowVisualizer.goBackInCommandMenu.click(); + + const openInFullScreenButton = workflowVisualizer.commandMenu.getByRole( + 'button', + { name: 'Open' }, + ); + + await openInFullScreenButton.click(); + + const workflowRunNameInShowPage = page + .getByText(`#1 - ${workflowVisualizer.workflowName}`) + .nth(1); + + await expect(workflowRunNameInShowPage).toBeVisible(); + + // Expect the side panel to be opened by default on the form. + await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText( + 'Form', + ); +}); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts index 5a375b8b9..f46d77cfe 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts @@ -19,7 +19,6 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId'; import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; -import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId'; import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowServerlessFunctionTabListComponentId'; import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowServerlessFunctionTabId'; import { useRecoilCallback } from 'recoil'; @@ -61,12 +60,6 @@ export const useCommandMenuCloseAnimationCompleteCleanup = () => { emitRightDrawerCloseEvent(); set(isCommandMenuClosingState, false); - set( - activeTabIdComponentState.atomFamily({ - instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID, - }), - null, - ); set( activeTabIdComponentState.atomFamily({ instanceId: WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID, diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordInCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordInCommandMenu.ts index 81d986b60..00367df94 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordInCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordInCommandMenu.ts @@ -11,21 +11,25 @@ import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-sto import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { getIconColorForObjectType } from '@/object-metadata/utils/getIconColorForObjectType'; import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; +import { useRunWorkflowRunOpeningInCommandMenuSideEffects } from '@/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects'; import { useTheme } from '@emotion/react'; import { t } from '@lingui/core/macro'; import { useRecoilCallback } from 'recoil'; -import { v4 } from 'uuid'; import { capitalize } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; +import { v4 } from 'uuid'; export const useOpenRecordInCommandMenu = () => { - const { navigateCommandMenu } = useCommandMenu(); - const theme = useTheme(); const { getIcon } = useIcons(); + const { navigateCommandMenu } = useCommandMenu(); + const { runWorkflowRunOpeningInCommandMenuSideEffects } = + useRunWorkflowRunOpeningInCommandMenuSideEffects(); + const openRecordInCommandMenu = useRecoilCallback( ({ set, snapshot }) => { return ({ @@ -147,9 +151,21 @@ export const useOpenRecordInCommandMenu = () => { pageId: pageComponentInstanceId, resetNavigationStack: false, }); + + if (objectNameSingular === CoreObjectNameSingular.WorkflowRun) { + runWorkflowRunOpeningInCommandMenuSideEffects({ + objectMetadataItem, + recordId, + }); + } }; }, - [getIcon, navigateCommandMenu, theme], + [ + getIcon, + navigateCommandMenu, + runWorkflowRunOpeningInCommandMenuSideEffects, + theme, + ], ); return { diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts index fb36c68c4..0ecedbc0e 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts @@ -1,17 +1,21 @@ import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; import { workflowIdComponentState } from '@/command-menu/pages/workflow/states/workflowIdComponentState'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { useSetInitialWorkflowRunRightDrawerTab } from '@/workflow/workflow-diagram/hooks/useSetInitialWorkflowRunRightDrawerTab'; +import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { t } from '@lingui/core/macro'; import { useRecoilCallback } from 'recoil'; -import { v4 } from 'uuid'; import { IconBolt, IconComponent, IconSettingsAutomation, } from 'twenty-ui/display'; +import { v4 } from 'uuid'; export const useWorkflowCommandMenu = () => { const { navigateCommandMenu } = useNavigateCommandMenu(); + const { setInitialWorkflowRunRightDrawerTab } = + useSetInitialWorkflowRunRightDrawerTab(); const openWorkflowTriggerTypeInCommandMenu = useRecoilCallback( ({ set }) => { @@ -99,7 +103,19 @@ export const useWorkflowCommandMenu = () => { const openWorkflowRunViewStepInCommandMenu = useRecoilCallback( ({ set }) => { - return (workflowId: string, title: string, icon: IconComponent) => { + return ({ + workflowId, + title, + icon, + workflowSelectedNode, + stepExecutionStatus, + }: { + workflowId: string; + title: string; + icon: IconComponent; + workflowSelectedNode: string; + stepExecutionStatus: WorkflowDiagramRunStatus; + }) => { const pageId = v4(); set( @@ -113,9 +129,14 @@ export const useWorkflowCommandMenu = () => { pageIcon: icon, pageId, }); + + setInitialWorkflowRunRightDrawerTab({ + workflowSelectedNode, + stepExecutionStatus, + }); }; }, - [navigateCommandMenu], + [navigateCommandMenu, setInitialWorkflowRunRightDrawerTab], ); return { diff --git a/packages/twenty-front/src/modules/command-menu/pages/workflow/step/view-run/components/CommandMenuWorkflowRunViewStep.tsx b/packages/twenty-front/src/modules/command-menu/pages/workflow/step/view-run/components/CommandMenuWorkflowRunViewStep.tsx index f0fe16e58..6f34c1fc1 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/workflow/step/view-run/components/CommandMenuWorkflowRunViewStep.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/workflow/step/view-run/components/CommandMenuWorkflowRunViewStep.tsx @@ -1,7 +1,9 @@ import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled'; import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled'; +import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext'; import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList'; import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState'; +import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow'; import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun'; @@ -11,13 +13,13 @@ import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hook import { WorkflowRunStepInputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepInputDetail'; import { WorkflowRunStepNodeDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepNodeDetail'; import { WorkflowRunStepOutputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepOutputDetail'; -import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId'; import { WorkflowRunTabId, WorkflowRunTabIdType, } from '@/workflow/workflow-steps/types/WorkflowRunTabId'; import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus'; import styled from '@emotion/styled'; +import { isNull } from '@sniptt/guards'; import { isDefined } from 'twenty-shared/utils'; import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui/display'; @@ -41,9 +43,18 @@ export const CommandMenuWorkflowRunViewStep = () => { const workflowRun = useWorkflowRun({ workflowRunId }); + const commandMenuPageComponentInstance = useComponentInstanceStateContext( + CommandMenuPageComponentInstanceContext, + ); + if (isNull(commandMenuPageComponentInstance)) { + throw new Error( + 'CommandMenuPageComponentInstanceContext is not defined. This component should be used within CommandMenuPageComponentInstanceContext.', + ); + } + const activeTabId = useRecoilComponentValueV2( activeTabIdComponentState, - WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID, + commandMenuPageComponentInstance.instanceId, ); if (!isDefined(workflowRun)) { @@ -90,9 +101,7 @@ export const CommandMenuWorkflowRunViewStep = () => { {activeTabId === WorkflowRunTabId.OUTPUT ? ( 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 3e58a88e0..02f8110b0 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 @@ -145,7 +145,7 @@ export const useRecordShowContainerTabs = ( tabs: { workflow: { title: 'Flow', - position: 0, + position: 101, Icon: IconSettings, cards: [{ type: CardType.WorkflowCard }], hide: { @@ -168,7 +168,7 @@ export const useRecordShowContainerTabs = ( tabs: { workflowVersion: { title: 'Flow', - position: 0, + position: 101, Icon: IconSettings, cards: [{ type: CardType.WorkflowVersionCard }], hide: { @@ -190,7 +190,7 @@ export const useRecordShowContainerTabs = ( tabs: { workflowRun: { title: 'Flow', - position: 0, + position: 101, Icon: IconSettings, cards: [{ type: CardType.WorkflowRunCard }], hide: { diff --git a/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects.ts b/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects.ts new file mode 100644 index 000000000..9a233a042 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects.ts @@ -0,0 +1,84 @@ +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { flowState } from '@/workflow/states/flowState'; +import { workflowIdState } from '@/workflow/states/workflowIdState'; +import { workflowRunIdState } from '@/workflow/states/workflowRunIdState'; +import { WorkflowRun } from '@/workflow/types/Workflow'; +import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState'; +import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram'; +import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; +import { useApolloClient } from '@apollo/client'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; + +export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => { + const apolloClient = useApolloClient(); + const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu(); + const { getIcon } = useIcons(); + + const runWorkflowRunOpeningInCommandMenuSideEffects = useRecoilCallback( + ({ snapshot, set }) => + ({ + objectMetadataItem, + recordId, + }: { + objectMetadataItem: ObjectMetadataItem; + recordId: string; + }) => { + const objectMetadataItems = getSnapshotValue( + snapshot, + objectMetadataItemsState, + ); + + const workflowRunRecord = getRecordFromCache({ + objectMetadataItem, + cache: apolloClient.cache, + recordId, + objectMetadataItems, + }); + if ( + !(isDefined(workflowRunRecord) && isDefined(workflowRunRecord.output)) + ) { + throw new Error( + `No workflow run record found for record ID ${recordId}`, + ); + } + + const { stepToOpenByDefault } = generateWorkflowRunDiagram({ + steps: workflowRunRecord.output.flow.steps, + stepsOutput: workflowRunRecord.output.stepsOutput, + trigger: workflowRunRecord.output.flow.trigger, + }); + + if (!isDefined(stepToOpenByDefault)) { + return; + } + + set(workflowRunIdState, workflowRunRecord.id); + set(workflowIdState, workflowRunRecord.workflowId); + set(flowState, { + workflowVersionId: workflowRunRecord.workflowVersionId, + trigger: workflowRunRecord.output.flow.trigger, + steps: workflowRunRecord.output.flow.steps, + }); + set(workflowSelectedNodeState, stepToOpenByDefault.id); + + openWorkflowRunViewStepInCommandMenu({ + workflowId: workflowRunRecord.workflowId, + title: stepToOpenByDefault.data.name, + icon: getIcon(getWorkflowNodeIconKey(stepToOpenByDefault.data)), + workflowSelectedNode: stepToOpenByDefault.id, + stepExecutionStatus: stepToOpenByDefault.data.runStatus, + }); + }, + [apolloClient.cache, getIcon, openWorkflowRunViewStepInCommandMenu], + ); + + return { + runWorkflowRunOpeningInCommandMenuSideEffects, + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.ts b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.ts index 7b5dff362..860f21d35 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.ts @@ -2,6 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { WorkflowRun } from '@/workflow/types/Workflow'; import { workflowRunSchema } from '@/workflow/validation-schemas/workflowSchema'; +import { useMemo } from 'react'; export const useWorkflowRun = ({ workflowRunId, @@ -13,7 +14,10 @@ export const useWorkflowRun = ({ objectRecordId: workflowRunId, }); - const { success, data: record } = workflowRunSchema.safeParse(rawRecord); + const { success, data: record } = useMemo( + () => workflowRunSchema.safeParse(rawRecord), + [rawRecord], + ); if (!success) { return undefined; diff --git a/packages/twenty-front/src/modules/workflow/states/workflowRunIdState.ts b/packages/twenty-front/src/modules/workflow/states/workflowRunIdState.ts index d807d2af7..f1460ff00 100644 --- a/packages/twenty-front/src/modules/workflow/states/workflowRunIdState.ts +++ b/packages/twenty-front/src/modules/workflow/states/workflowRunIdState.ts @@ -1,4 +1,5 @@ import { createState } from 'twenty-ui/utilities'; + export const workflowRunIdState = createState({ key: 'workflowRunIdState', defaultValue: undefined, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx index 2e6d00c0c..5d517e832 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx @@ -26,10 +26,10 @@ import { } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import React, { useEffect, useMemo, useRef } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; -import { THEME_COMMON } from 'twenty-ui/theme'; import { Tag, TagColor } from 'twenty-ui/components'; +import { THEME_COMMON } from 'twenty-ui/theme'; const StyledResetReactflowStyles = styled.div` height: 100%; @@ -72,7 +72,7 @@ const StyledStatusTagContainer = styled.div` left: 0; top: 0; position: absolute; - padding: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(4)}; `; const defaultFitViewOptions = { @@ -87,6 +87,7 @@ export const WorkflowDiagramCanvasBase = ({ tagContainerTestId, tagColor, tagText, + onInit, }: { nodeTypes: Partial< Record< @@ -114,13 +115,11 @@ export const WorkflowDiagramCanvasBase = ({ tagContainerTestId: string; tagColor: TagColor; tagText: string; + onInit?: () => void; }) => { const theme = useTheme(); const reactflow = useReactFlow(); - const setWorkflowReactFlowRefState = useSetRecoilState( - workflowReactFlowRefState, - ); const workflowDiagram = useRecoilValue(workflowDiagramState); @@ -140,22 +139,13 @@ export const WorkflowDiagramCanvasBase = ({ const setWorkflowDiagram = useSetRecoilState(workflowDiagramState); - const handleNodesChange = ( - nodeChanges: Array>, - ) => { - setWorkflowDiagram((diagram) => { - if (isDefined(diagram) === false) { - throw new Error( - 'It must be impossible for the nodes to be updated if the diagram is not defined yet. Be sure the diagram is rendered only when defined.', - ); - } - - return { - ...diagram, - nodes: applyNodeChanges(nodeChanges, diagram.nodes), - }; - }); - }; + const setWorkflowReactFlowRef = useRecoilCallback( + ({ set }) => + (node: HTMLDivElement | null) => { + set(workflowReactFlowRefState, { current: node }); + }, + [], + ); const handleEdgesChange = ( edgeChanges: Array>, @@ -209,36 +199,55 @@ export const WorkflowDiagramCanvasBase = ({ ); }, [reactflow, rightDrawerState, rightDrawerWidth]); + const handleNodesChanges = useRecoilCallback( + ({ set }) => + (changes: NodeChange[]) => { + set(workflowDiagramState, (diagram) => { + if (!isDefined(diagram)) { + throw new Error( + 'It must be impossible for the nodes to be updated if the diagram is not defined yet. Be sure the diagram is rendered only when defined.', + ); + } + + return { + ...diagram, + nodes: applyNodeChanges(changes, diagram.nodes), + }; + }); + }, + [], + ); + + const handleInit = () => { + if (!isDefined(containerRef.current)) { + return; + } + + const flowBounds = reactflow.getNodesBounds(reactflow.getNodes()); + + reactflow.setViewport({ + x: containerRef.current.offsetWidth / 2 - flowBounds.width / 2, + y: 150, + zoom: defaultFitViewOptions.maxZoom, + }); + + onInit?.(); + }; + return ( { - if (isDefined(node)) { - setWorkflowReactFlowRefState({ current: node }); - } - }} - onInit={() => { - if (!isDefined(containerRef.current)) { - throw new Error('Expect the container ref to be defined'); - } - - const flowBounds = reactflow.getNodesBounds(reactflow.getNodes()); - - reactflow.setViewport({ - x: containerRef.current.offsetWidth / 2 - flowBounds.width / 2, - y: 150, - zoom: defaultFitViewOptions.maxZoom, - }); - }} + ref={setWorkflowReactFlowRef} + onInit={handleInit} minZoom={defaultFitViewOptions.minZoom} maxZoom={defaultFitViewOptions.maxZoom} nodeTypes={nodeTypes} edgeTypes={edgeTypes} nodes={nodes} edges={edges} - onNodesChange={handleNodesChange} + onNodesChange={handleNodesChanges} onEdgesChange={handleEdgesChange} onBeforeDelete={async () => { // Abort all non-programmatic deletions @@ -251,6 +260,7 @@ export const WorkflowDiagramCanvasBase = ({ nodesDraggable={false} nodesConnectable={false} paneClickDistance={10} // Fix small unwanted user dragging does not select node + preventScrolling={false} > diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx index 2c4c9c5a2..1d3006d67 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx @@ -4,6 +4,7 @@ import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/componen import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly'; import { WorkflowDiagramSuccessEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge'; import { WorkflowRunDiagramCanvasEffect } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect'; +import { useHandleWorkflowRunDiagramCanvasInit } from '@/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit'; import { getWorkflowRunStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps'; import { ReactFlowProvider } from '@xyflow/react'; @@ -16,6 +17,9 @@ export const WorkflowRunDiagramCanvas = ({ workflowRunStatus, }); + const { handleWorkflowRunDiagramCanvasInit } = + useHandleWorkflowRunDiagramCanvasInit(); + return ( diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx index 16d1123b7..c524fa985 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx @@ -1,121 +1,61 @@ import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; -import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled'; -import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled'; -import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { workflowIdState } from '@/workflow/states/workflowIdState'; +import { workflowDiagramStatusState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusState'; import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState'; -import { - WorkflowDiagramNode, - WorkflowDiagramRunStatus, - WorkflowRunDiagramStepNodeData, -} from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { WorkflowRunDiagramNode } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; -import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId'; -import { WorkflowRunTabId } from '@/workflow/workflow-steps/types/WorkflowRunTabId'; -import { isNull } from '@sniptt/guards'; import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react'; -import { useCallback } from 'react'; -import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; export const WorkflowRunDiagramCanvasEffect = () => { const { getIcon } = useIcons(); - const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState); + const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu(); - const workflowId = useRecoilValue(workflowIdState); - - const resetWorkflowRunRightDrawerTabIfNeeded = useRecoilCallback( + const handleSelectionChange = useRecoilCallback( ({ snapshot, set }) => - ({ - workflowSelectedNode, - stepExecutionStatus, - }: { - workflowSelectedNode: string; - stepExecutionStatus: WorkflowDiagramRunStatus; - }) => { - const activeWorkflowRunRightDrawerTab = getSnapshotValue( + ({ nodes }: OnSelectionChangeParams) => { + const workflowId = getSnapshotValue(snapshot, workflowIdState); + + if (!isDefined(workflowId)) { + throw new Error('Expected the workflowId to be defined.'); + } + + const workflowDiagramStatus = getSnapshotValue( snapshot, - activeTabIdComponentState.atomFamily({ - instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID, - }), - ) as WorkflowRunTabId | null; - - const isInputTabDisabled = getIsInputTabDisabled({ - stepExecutionStatus, - workflowSelectedNode, - }); - const isOutputTabDisabled = getIsOutputTabDisabled({ - stepExecutionStatus, - }); - - if (isNull(activeWorkflowRunRightDrawerTab)) { - const defaultTabId = isOutputTabDisabled - ? WorkflowRunTabId.NODE - : WorkflowRunTabId.OUTPUT; - - set( - activeTabIdComponentState.atomFamily({ - instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID, - }), - defaultTabId, - ); + workflowDiagramStatusState, + ); + // The `handleSelectionChange` function is called when the diagram initializes and + // a node is selected. In this case, we don't want to execute the rest of this function. + // We open the Side PanelĀ® synchronously after ReactFlow is initialized and a node is selected, + // animations perform better that way. + if (workflowDiagramStatus !== 'done') { return; } - if ( - (isInputTabDisabled && - activeWorkflowRunRightDrawerTab === WorkflowRunTabId.INPUT) || - (isOutputTabDisabled && - activeWorkflowRunRightDrawerTab === WorkflowRunTabId.OUTPUT) - ) { - set( - activeTabIdComponentState.atomFamily({ - instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID, - }), - WorkflowRunTabId.NODE, - ); + const selectedNode = nodes[0] as WorkflowRunDiagramNode | undefined; + + if (!isDefined(selectedNode)) { + return; } - }, - [], - ); - const handleSelectionChange = useCallback( - ({ nodes }: OnSelectionChangeParams) => { - const selectedNode = nodes[0] as WorkflowDiagramNode | undefined; + set(workflowSelectedNodeState, selectedNode.id); - if (!isDefined(selectedNode)) { - return; - } + const selectedNodeData = selectedNode.data; - setWorkflowSelectedNode(selectedNode.id); - - const selectedNodeData = - selectedNode.data as WorkflowRunDiagramStepNodeData; - - if (isDefined(workflowId)) { - openWorkflowRunViewStepInCommandMenu( + openWorkflowRunViewStepInCommandMenu({ workflowId, - selectedNodeData.name, - getIcon(getWorkflowNodeIconKey(selectedNodeData)), - ); - - resetWorkflowRunRightDrawerTabIfNeeded({ + title: selectedNodeData.name, + icon: getIcon(getWorkflowNodeIconKey(selectedNodeData)), workflowSelectedNode: selectedNode.id, stepExecutionStatus: selectedNodeData.runStatus, }); - } - }, - [ - setWorkflowSelectedNode, - resetWorkflowRunRightDrawerTabIfNeeded, - workflowId, - openWorkflowRunViewStepInCommandMenu, - getIcon, - ], + }, + [getIcon, openWorkflowRunViewStepInCommandMenu], ); useOnSelectionChange({ diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizer.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizer.tsx index 3627978c6..a46d913c7 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizer.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizer.tsx @@ -1,6 +1,8 @@ import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun'; import { WorkflowRunDiagramCanvas } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas'; +import { workflowDiagramStatusState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusState'; import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; const StyledContainer = styled.div` @@ -13,8 +15,12 @@ export const WorkflowRunVisualizer = ({ workflowRunId: string; }) => { const workflowRun = useWorkflowRun({ workflowRunId }); + const workflowDiagramStatus = useRecoilValue(workflowDiagramStatusState); - if (!isDefined(workflowRun)) { + if ( + !isDefined(workflowRun) || + workflowDiagramStatus === 'computing-diagram' + ) { return null; } 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 index a122191c1..c49777bc9 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx @@ -1,13 +1,18 @@ +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { useStepsOutputSchema } from '@/workflow/hooks/useStepsOutputSchema'; import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun'; import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion'; import { flowState } from '@/workflow/states/flowState'; import { workflowIdState } from '@/workflow/states/workflowIdState'; import { workflowRunIdState } from '@/workflow/states/workflowRunIdState'; +import { WorkflowRunOutput } from '@/workflow/types/Workflow'; import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflowDiagramState'; +import { workflowDiagramStatusState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusState'; +import { workflowRunStepToOpenByDefaultState } from '@/workflow/workflow-diagram/states/workflowRunStepToOpenByDefaultState'; import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram'; -import { useEffect } from 'react'; -import { useSetRecoilState } from 'recoil'; +import { selectWorkflowDiagramNode } from '@/workflow/workflow-diagram/utils/selectWorkflowDiagramNode'; +import { useContext, useEffect } from 'react'; +import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; export const WorkflowRunVisualizerEffect = ({ @@ -20,10 +25,10 @@ export const WorkflowRunVisualizerEffect = ({ const setWorkflowRunId = useSetRecoilState(workflowRunIdState); const setWorkflowId = useSetRecoilState(workflowIdState); - const setFlow = useSetRecoilState(flowState); - const setWorkflowDiagram = useSetRecoilState(workflowDiagramState); const { populateStepsOutputSchema } = useStepsOutputSchema(); + const { isInRightDrawer } = useContext(ActionMenuContext); + useEffect(() => { setWorkflowRunId(workflowRunId); }, [setWorkflowRunId, workflowRunId]); @@ -32,33 +37,72 @@ export const WorkflowRunVisualizerEffect = ({ if (!isDefined(workflowRun)) { return; } + setWorkflowId(workflowRun.workflowId); }, [setWorkflowId, workflowRun]); + const handleWorkflowRunDiagramGeneration = useRecoilCallback( + ({ set }) => + ({ + workflowRunOutput, + workflowVersionId, + skipNodeSelection, + }: { + workflowRunOutput: WorkflowRunOutput | undefined; + workflowVersionId: string | undefined; + skipNodeSelection: boolean; + }) => { + if (!(isDefined(workflowRunOutput) && isDefined(workflowVersionId))) { + set(flowState, undefined); + set(workflowDiagramState, undefined); + + return; + } + + set(workflowDiagramStatusState, 'computing-diagram'); + + set(flowState, { + workflowVersionId, + trigger: workflowRunOutput.flow.trigger, + steps: workflowRunOutput.flow.steps, + }); + + const { diagram: baseWorkflowRunDiagram, stepToOpenByDefault } = + generateWorkflowRunDiagram({ + trigger: workflowRunOutput.flow.trigger, + steps: workflowRunOutput.flow.steps, + stepsOutput: workflowRunOutput.stepsOutput, + }); + + if (isDefined(stepToOpenByDefault) && !skipNodeSelection) { + const workflowRunDiagram = selectWorkflowDiagramNode({ + diagram: baseWorkflowRunDiagram, + nodeIdToSelect: stepToOpenByDefault.id, + }); + + set(workflowDiagramState, workflowRunDiagram); + set(workflowRunStepToOpenByDefaultState, { + id: stepToOpenByDefault.id, + data: stepToOpenByDefault.data, + }); + } else { + set(workflowDiagramState, baseWorkflowRunDiagram); + } + + set(workflowDiagramStatusState, 'computing-dimensions'); + }, + [], + ); + useEffect(() => { - if (!isDefined(workflowRun?.output)) { - setFlow(undefined); - setWorkflowDiagram(undefined); - - return; - } - - setFlow({ - workflowVersionId: workflowRun.workflowVersionId, - trigger: workflowRun.output.flow.trigger, - steps: workflowRun.output.flow.steps, + handleWorkflowRunDiagramGeneration({ + workflowRunOutput: workflowRun?.output ?? undefined, + workflowVersionId: workflowRun?.workflowVersionId, + skipNodeSelection: isInRightDrawer, }); - - const nextWorkflowDiagram = generateWorkflowRunDiagram({ - trigger: workflowRun.output.flow.trigger, - steps: workflowRun.output.flow.steps, - stepsOutput: workflowRun.output.stepsOutput, - }); - - setWorkflowDiagram(nextWorkflowDiagram); }, [ - setFlow, - setWorkflowDiagram, + handleWorkflowRunDiagramGeneration, + isInRightDrawer, workflowRun?.output, workflowRun?.workflowVersionId, ]); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit.ts new file mode 100644 index 000000000..ab8849c84 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit.ts @@ -0,0 +1,74 @@ +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; +import { workflowIdState } from '@/workflow/states/workflowIdState'; +import { workflowDiagramStatusState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusState'; +import { workflowRunStepToOpenByDefaultState } from '@/workflow/workflow-diagram/states/workflowRunStepToOpenByDefaultState'; +import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState'; +import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; +import { useContext } from 'react'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; + +export const useHandleWorkflowRunDiagramCanvasInit = () => { + const { getIcon } = useIcons(); + + const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu(); + const { isInRightDrawer } = useContext(ActionMenuContext); + + const handleWorkflowRunDiagramCanvasInit = useRecoilCallback( + ({ snapshot, set }) => + () => { + const workflowDiagramStatus = getSnapshotValue( + snapshot, + workflowDiagramStatusState, + ); + + if (workflowDiagramStatus !== 'computing-dimensions') { + throw new Error( + 'Sequence error: reactflow should be considered initialized only when the workflow diagram status is computing-dimensions.', + ); + } + + set(workflowDiagramStatusState, 'done'); + + if (isInRightDrawer) { + return; + } + + const workflowStepToOpenByDefault = getSnapshotValue( + snapshot, + workflowRunStepToOpenByDefaultState, + ); + + if (isDefined(workflowStepToOpenByDefault)) { + const workflowId = getSnapshotValue(snapshot, workflowIdState); + if (!isDefined(workflowId)) { + throw new Error( + 'The workflow id must be set; ensure the workflow id is always set before rendering the workflow diagram.', + ); + } + + set(workflowSelectedNodeState, workflowStepToOpenByDefault.id); + + openWorkflowRunViewStepInCommandMenu({ + workflowId, + title: workflowStepToOpenByDefault.data.name, + icon: getIcon( + getWorkflowNodeIconKey(workflowStepToOpenByDefault.data), + ), + workflowSelectedNode: workflowStepToOpenByDefault.id, + stepExecutionStatus: workflowStepToOpenByDefault.data.runStatus, + }); + + set(workflowRunStepToOpenByDefaultState, undefined); + } + }, + [getIcon, isInRightDrawer, openWorkflowRunViewStepInCommandMenu], + ); + + return { + handleWorkflowRunDiagramCanvasInit, + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useSetInitialWorkflowRunRightDrawerTab.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useSetInitialWorkflowRunRightDrawerTab.ts new file mode 100644 index 000000000..06ecf013e --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useSetInitialWorkflowRunRightDrawerTab.ts @@ -0,0 +1,76 @@ +import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled'; +import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; +import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { WorkflowRunTabId } from '@/workflow/workflow-steps/types/WorkflowRunTabId'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +export const useSetInitialWorkflowRunRightDrawerTab = () => { + const setInitialWorkflowRunRightDrawerTab = useRecoilCallback( + ({ snapshot, set }) => + ({ + workflowSelectedNode, + stepExecutionStatus, + }: { + workflowSelectedNode: string; + stepExecutionStatus: WorkflowDiagramRunStatus; + }) => { + const commandMenuPageInfo = getSnapshotValue( + snapshot, + commandMenuPageInfoState, + ); + + const activeWorkflowRunRightDrawerTab = getSnapshotValue( + snapshot, + activeTabIdComponentState.atomFamily({ + instanceId: commandMenuPageInfo.instanceId, + }), + ) as WorkflowRunTabId | null; + + const isInputTabDisabled = getIsInputTabDisabled({ + stepExecutionStatus, + workflowSelectedNode, + }); + const isOutputTabDisabled = getIsOutputTabDisabled({ + stepExecutionStatus, + }); + + if (!isDefined(activeWorkflowRunRightDrawerTab)) { + const defaultTabId = isOutputTabDisabled + ? WorkflowRunTabId.NODE + : WorkflowRunTabId.OUTPUT; + + set( + activeTabIdComponentState.atomFamily({ + instanceId: commandMenuPageInfo.instanceId, + }), + defaultTabId, + ); + + return; + } + + if ( + (isInputTabDisabled && + activeWorkflowRunRightDrawerTab === WorkflowRunTabId.INPUT) || + (isOutputTabDisabled && + activeWorkflowRunRightDrawerTab === WorkflowRunTabId.OUTPUT) + ) { + set( + activeTabIdComponentState.atomFamily({ + instanceId: commandMenuPageInfo.instanceId, + }), + WorkflowRunTabId.NODE, + ); + } + }, + [], + ); + + return { + setInitialWorkflowRunRightDrawerTab, + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowDiagramStatusState.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowDiagramStatusState.ts new file mode 100644 index 000000000..6a43fac9f --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowDiagramStatusState.ts @@ -0,0 +1,8 @@ +import { createState } from 'twenty-ui/utilities'; + +export const workflowDiagramStatusState = createState< + 'computing-diagram' | 'computing-dimensions' | 'done' +>({ + key: 'workflowDiagramStatusState', + defaultValue: 'computing-diagram', +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowRunStepToOpenByDefaultState.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowRunStepToOpenByDefaultState.ts new file mode 100644 index 000000000..d17465faf --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowRunStepToOpenByDefaultState.ts @@ -0,0 +1,13 @@ +import { WorkflowRunDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { createState } from 'twenty-ui/utilities'; + +export const workflowRunStepToOpenByDefaultState = createState< + | { + id: string; + data: WorkflowRunDiagramStepNodeData; + } + | undefined +>({ + key: 'workflowStepIdToOpenByDefaultState', + defaultValue: undefined, +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowSelectedNodeState.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowSelectedNodeState.ts index 720c3e7d2..6046be903 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowSelectedNodeState.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowSelectedNodeState.ts @@ -1,4 +1,5 @@ import { createState } from 'twenty-ui/utilities'; + export const workflowSelectedNodeState = createState({ key: 'workflowSelectedNodeState', defaultValue: undefined, 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 index 4bf33eea6..86f42e1ee 100644 --- 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 @@ -3,6 +3,7 @@ import { WorkflowStep, WorkflowTrigger, } from '@/workflow/types/Workflow'; +import { FieldMetadataType } from 'twenty-shared/types'; import { getUuidV4Mock } from '~/testing/utils/getUuidV4Mock'; import { generateWorkflowRunDiagram } from '../generateWorkflowRunDiagram'; @@ -87,94 +88,94 @@ describe('generateWorkflowRunDiagram', () => { 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", - "name": "Company created", - "nodeType": "trigger", - "runStatus": "success", - "triggerType": "DATABASE_EVENT", + "diagram": { + "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", }, - "id": "trigger", - "position": { - "x": 0, - "y": 0, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-1", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", + "selectable": false, + "source": "step1", + "target": "step2", }, - }, - { - "data": { - "actionType": "CODE", - "name": "Step 1", - "nodeType": "action", - "runStatus": "failure", + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-2", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", + "selectable": false, + "source": "step2", + "target": "step3", }, - "id": "step1", - "position": { - "x": 0, - "y": 0, + ], + "nodes": [ + { + "data": { + "icon": "IconPlaylistAdd", + "name": "Company created", + "nodeType": "trigger", + "runStatus": "success", + "triggerType": "DATABASE_EVENT", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, }, - "selected": false, - }, - { - "data": { - "actionType": "CODE", - "name": "Step 2", - "nodeType": "action", - "runStatus": "not-executed", + { + "data": { + "actionType": "CODE", + "name": "Step 1", + "nodeType": "action", + "runStatus": "failure", + }, + "id": "step1", + "position": { + "x": 0, + "y": 0, + }, }, - "id": "step2", - "position": { - "x": 0, - "y": 150, + { + "data": { + "actionType": "CODE", + "name": "Step 2", + "nodeType": "action", + "runStatus": "not-executed", + }, + "id": "step2", + "position": { + "x": 0, + "y": 150, + }, }, - "selected": false, - }, - { - "data": { - "actionType": "CODE", - "name": "Step 3", - "nodeType": "action", - "runStatus": "not-executed", + { + "data": { + "actionType": "CODE", + "name": "Step 3", + "nodeType": "action", + "runStatus": "not-executed", + }, + "id": "step3", + "position": { + "x": 0, + "y": 300, + }, }, - "id": "step3", - "position": { - "x": 0, - "y": 300, - }, - "selected": false, - }, - ], + ], + }, + "stepToOpenByDefault": undefined, } `); }); @@ -263,96 +264,96 @@ describe('generateWorkflowRunDiagram', () => { 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-green-arrow-rounded", - "markerStart": "workflow-edge-green-circle", - "selectable": false, - "source": "step1", - "target": "step2", - "type": "success", - }, - { - "deletable": false, - "id": "8f3b2121-f194-4ba4-9fbf-5", - "markerEnd": "workflow-edge-green-arrow-rounded", - "markerStart": "workflow-edge-green-circle", - "selectable": false, - "source": "step2", - "target": "step3", - "type": "success", - }, - ], - "nodes": [ - { - "data": { - "icon": "IconPlaylistAdd", - "name": "Company created", - "nodeType": "trigger", - "runStatus": "success", - "triggerType": "DATABASE_EVENT", + "diagram": { + "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", }, - "id": "trigger", - "position": { - "x": 0, - "y": 0, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-4", + "markerEnd": "workflow-edge-green-arrow-rounded", + "markerStart": "workflow-edge-green-circle", + "selectable": false, + "source": "step1", + "target": "step2", + "type": "success", }, - }, - { - "data": { - "actionType": "CODE", - "name": "Step 1", - "nodeType": "action", - "runStatus": "success", + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-5", + "markerEnd": "workflow-edge-green-arrow-rounded", + "markerStart": "workflow-edge-green-circle", + "selectable": false, + "source": "step2", + "target": "step3", + "type": "success", }, - "id": "step1", - "position": { - "x": 0, - "y": 0, + ], + "nodes": [ + { + "data": { + "icon": "IconPlaylistAdd", + "name": "Company created", + "nodeType": "trigger", + "runStatus": "success", + "triggerType": "DATABASE_EVENT", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, }, - "selected": false, - }, - { - "data": { - "actionType": "CODE", - "name": "Step 2", - "nodeType": "action", - "runStatus": "success", + { + "data": { + "actionType": "CODE", + "name": "Step 1", + "nodeType": "action", + "runStatus": "success", + }, + "id": "step1", + "position": { + "x": 0, + "y": 0, + }, }, - "id": "step2", - "position": { - "x": 0, - "y": 150, + { + "data": { + "actionType": "CODE", + "name": "Step 2", + "nodeType": "action", + "runStatus": "success", + }, + "id": "step2", + "position": { + "x": 0, + "y": 150, + }, }, - "selected": false, - }, - { - "data": { - "actionType": "CODE", - "name": "Step 3", - "nodeType": "action", - "runStatus": "success", + { + "data": { + "actionType": "CODE", + "name": "Step 3", + "nodeType": "action", + "runStatus": "success", + }, + "id": "step3", + "position": { + "x": 0, + "y": 300, + }, }, - "id": "step3", - "position": { - "x": 0, - "y": 300, - }, - "selected": false, - }, - ], + ], + }, + "stepToOpenByDefault": undefined, } `); }); @@ -428,94 +429,94 @@ describe('generateWorkflowRunDiagram', () => { 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-arrow-rounded", - "markerStart": "workflow-edge-gray-circle", - "selectable": false, - "source": "step1", - "target": "step2", - }, - { - "deletable": false, - "id": "8f3b2121-f194-4ba4-9fbf-8", - "markerEnd": "workflow-edge-arrow-rounded", - "markerStart": "workflow-edge-gray-circle", - "selectable": false, - "source": "step2", - "target": "step3", - }, - ], - "nodes": [ - { - "data": { - "icon": "IconPlaylistAdd", - "name": "Company created", - "nodeType": "trigger", - "runStatus": "success", - "triggerType": "DATABASE_EVENT", + "diagram": { + "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", }, - "id": "trigger", - "position": { - "x": 0, - "y": 0, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-7", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", + "selectable": false, + "source": "step1", + "target": "step2", }, - }, - { - "data": { - "actionType": "CODE", - "name": "Step 1", - "nodeType": "action", - "runStatus": "running", + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-8", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", + "selectable": false, + "source": "step2", + "target": "step3", }, - "id": "step1", - "position": { - "x": 0, - "y": 0, + ], + "nodes": [ + { + "data": { + "icon": "IconPlaylistAdd", + "name": "Company created", + "nodeType": "trigger", + "runStatus": "success", + "triggerType": "DATABASE_EVENT", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, }, - "selected": false, - }, - { - "data": { - "actionType": "CODE", - "name": "Step 2", - "nodeType": "action", - "runStatus": "not-executed", + { + "data": { + "actionType": "CODE", + "name": "Step 1", + "nodeType": "action", + "runStatus": "running", + }, + "id": "step1", + "position": { + "x": 0, + "y": 0, + }, }, - "id": "step2", - "position": { - "x": 0, - "y": 150, + { + "data": { + "actionType": "CODE", + "name": "Step 2", + "nodeType": "action", + "runStatus": "not-executed", + }, + "id": "step2", + "position": { + "x": 0, + "y": 150, + }, }, - "selected": false, - }, - { - "data": { - "actionType": "CODE", - "name": "Step 3", - "nodeType": "action", - "runStatus": "not-executed", + { + "data": { + "actionType": "CODE", + "name": "Step 3", + "nodeType": "action", + "runStatus": "not-executed", + }, + "id": "step3", + "position": { + "x": 0, + "y": 300, + }, }, - "id": "step3", - "position": { - "x": 0, - "y": 300, - }, - "selected": false, - }, - ], + ], + }, + "stepToOpenByDefault": undefined, } `); }); @@ -614,118 +615,219 @@ describe('generateWorkflowRunDiagram', () => { 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", + "diagram": { + "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-green-arrow-rounded", + "markerStart": "workflow-edge-green-circle", + "selectable": false, + "source": "step1", + "target": "step2", + "type": "success", + }, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-11", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", + "selectable": false, + "source": "step2", + "target": "step3", + }, + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-12", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", + "selectable": false, + "source": "step3", + "target": "step4", + }, + ], + "nodes": [ + { + "data": { + "icon": "IconPlaylistAdd", + "name": "Company created", + "nodeType": "trigger", + "runStatus": "success", + "triggerType": "DATABASE_EVENT", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, + }, + { + "data": { + "actionType": "CODE", + "name": "Step 1", + "nodeType": "action", + "runStatus": "success", + }, + "id": "step1", + "position": { + "x": 0, + "y": 0, + }, + }, + { + "data": { + "actionType": "CODE", + "name": "Step 2", + "nodeType": "action", + "runStatus": "running", + }, + "id": "step2", + "position": { + "x": 0, + "y": 150, + }, + }, + { + "data": { + "actionType": "CODE", + "name": "Step 3", + "nodeType": "action", + "runStatus": "not-executed", + }, + "id": "step3", + "position": { + "x": 0, + "y": 300, + }, + }, + { + "data": { + "actionType": "CODE", + "name": "Step 4", + "nodeType": "action", + "runStatus": "not-executed", + }, + "id": "step4", + "position": { + "x": 0, + "y": 450, + }, + }, + ], + }, + "stepToOpenByDefault": undefined, +} +`); + }); + + it('marks node as running when a Form step is pending and return its data as the stepToOpenByDefault object', () => { + const trigger: WorkflowTrigger = { + name: 'Company created', + type: 'DATABASE_EVENT', + settings: { + eventName: 'company.created', + outputSchema: {}, + }, + }; + const steps: WorkflowStep[] = [ + { + id: 'step1', + name: 'Step 1', + type: 'FORM', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: [ + { + id: 'field-1', + name: 'text', + label: 'Text Field', + type: FieldMetadataType.TEXT, + placeholder: 'Enter text', + settings: {}, + }, + ], + outputSchema: {}, + }, + }, + ]; + const stepsOutput = { + step1: { + result: undefined, + error: undefined, + pendingEvent: true, + }, + }; + + const result = generateWorkflowRunDiagram({ trigger, steps, stepsOutput }); + + expect(result).toMatchInlineSnapshot(` +{ + "diagram": { + "edges": [ + { + "deletable": false, + "id": "8f3b2121-f194-4ba4-9fbf-13", + "markerEnd": "workflow-edge-green-arrow-rounded", + "markerStart": "workflow-edge-green-circle", + "selectable": false, + "source": "trigger", + "target": "step1", + "type": "success", + }, + ], + "nodes": [ + { + "data": { + "icon": "IconPlaylistAdd", + "name": "Company created", + "nodeType": "trigger", + "runStatus": "success", + "triggerType": "DATABASE_EVENT", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, + }, + { + "data": { + "actionType": "FORM", + "name": "Step 1", + "nodeType": "action", + "runStatus": "running", + }, + "id": "step1", + "position": { + "x": 0, + "y": 0, + }, + }, + ], + }, + "stepToOpenByDefault": { + "data": { + "actionType": "FORM", + "name": "Step 1", + "nodeType": "action", + "runStatus": "running", }, - { - "deletable": false, - "id": "8f3b2121-f194-4ba4-9fbf-10", - "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-11", - "markerEnd": "workflow-edge-arrow-rounded", - "markerStart": "workflow-edge-gray-circle", - "selectable": false, - "source": "step2", - "target": "step3", - }, - { - "deletable": false, - "id": "8f3b2121-f194-4ba4-9fbf-12", - "markerEnd": "workflow-edge-arrow-rounded", - "markerStart": "workflow-edge-gray-circle", - "selectable": false, - "source": "step3", - "target": "step4", - }, - ], - "nodes": [ - { - "data": { - "icon": "IconPlaylistAdd", - "name": "Company created", - "nodeType": "trigger", - "runStatus": "success", - "triggerType": "DATABASE_EVENT", - }, - "id": "trigger", - "position": { - "x": 0, - "y": 0, - }, - }, - { - "data": { - "actionType": "CODE", - "name": "Step 1", - "nodeType": "action", - "runStatus": "success", - }, - "id": "step1", - "position": { - "x": 0, - "y": 0, - }, - "selected": false, - }, - { - "data": { - "actionType": "CODE", - "name": "Step 2", - "nodeType": "action", - "runStatus": "running", - }, - "id": "step2", - "position": { - "x": 0, - "y": 150, - }, - "selected": false, - }, - { - "data": { - "actionType": "CODE", - "name": "Step 3", - "nodeType": "action", - "runStatus": "not-executed", - }, - "id": "step3", - "position": { - "x": 0, - "y": 300, - }, - "selected": false, - }, - { - "data": { - "actionType": "CODE", - "name": "Step 4", - "nodeType": "action", - "runStatus": "not-executed", - }, - "id": "step4", - "position": { - "x": 0, - "y": 450, - }, - "selected": false, - }, - ], + "id": "step1", + }, } `); }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/selectWorkflowDiagramNode.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/selectWorkflowDiagramNode.test.ts new file mode 100644 index 000000000..38bd8b80c --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/selectWorkflowDiagramNode.test.ts @@ -0,0 +1,91 @@ +import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { selectWorkflowDiagramNode } from '../selectWorkflowDiagramNode'; + +describe('selectWorkflowDiagramNode', () => { + it('should select the specified node', () => { + const diagram: WorkflowDiagram = { + nodes: [ + { + id: '1', + selected: false, + position: { x: 0, y: 0 }, + data: { + name: 'Node 1', + nodeType: 'action', + actionType: 'CODE', + }, + }, + { + id: '2', + selected: false, + position: { x: 0, y: 150 }, + data: { + name: 'Node 2', + nodeType: 'action', + actionType: 'CODE', + }, + }, + ], + edges: [], + }; + + const result = selectWorkflowDiagramNode({ + diagram, + nodeIdToSelect: '1', + }); + + expect(result.nodes[0].selected).toBe(true); + expect(result.nodes[1].selected).toBe(false); + }); + + it('should return same diagram when node is not found', () => { + const diagram: WorkflowDiagram = { + nodes: [ + { + id: '1', + selected: false, + position: { x: 0, y: 0 }, + data: { + name: 'Node 1', + nodeType: 'action', + actionType: 'CODE', + }, + }, + ], + edges: [], + }; + + const result = selectWorkflowDiagramNode({ + diagram, + nodeIdToSelect: 'non-existent', + }); + + expect(result).toEqual(diagram); + }); + + it('should not mutate original diagram', () => { + const diagram: WorkflowDiagram = { + nodes: [ + { + id: '1', + selected: false, + position: { x: 0, y: 0 }, + data: { + name: 'Node 1', + nodeType: 'action', + actionType: 'CODE', + }, + }, + ], + edges: [], + }; + const originalDiagram = JSON.parse(JSON.stringify(diagram)); + + selectWorkflowDiagramNode({ + diagram, + nodeIdToSelect: '1', + }); + + expect(diagram).toEqual(originalDiagram); + }); +}); 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 index cd7f5888a..1eca837e9 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowRunDiagram.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowRunDiagram.ts @@ -12,6 +12,8 @@ import { WorkflowRunDiagram, WorkflowRunDiagramEdge, WorkflowRunDiagramNode, + WorkflowRunDiagramNodeData, + WorkflowRunDiagramStepNodeData, } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode'; import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId'; @@ -26,7 +28,22 @@ export const generateWorkflowRunDiagram = ({ trigger: WorkflowTrigger; steps: Array; stepsOutput: WorkflowRunOutputStepsOutput | undefined; -}): WorkflowRunDiagram => { +}): { + diagram: WorkflowRunDiagram; + stepToOpenByDefault: + | { + id: string; + data: WorkflowRunDiagramStepNodeData; + } + | undefined; +} => { + let stepToOpenByDefault: + | { + id: string; + data: WorkflowRunDiagramStepNodeData; + } + | undefined = undefined; + const triggerBase = getWorkflowDiagramTriggerNode({ trigger }); const nodes: Array = [ @@ -97,21 +114,29 @@ export const generateWorkflowRunDiagram = ({ } } + const nodeData: WorkflowRunDiagramNodeData = { + nodeType: 'action', + actionType: step.type, + name: step.name, + runStatus, + }; + nodes.push({ id: nodeId, - data: { - nodeType: 'action', - actionType: step.type, - name: step.name, - runStatus, - }, + data: nodeData, position: { x: xPos, y: yPos, }, - selected: isPendingFormAction, }); + if (isPendingFormAction) { + stepToOpenByDefault = { + id: nodeId, + data: nodeData, + }; + } + processNode({ stepIndex: stepIndex + 1, parentNodeId: nodeId, @@ -134,7 +159,10 @@ export const generateWorkflowRunDiagram = ({ }); return { - nodes, - edges, + diagram: { + nodes, + edges, + }, + stepToOpenByDefault, }; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/selectWorkflowDiagramNode.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/selectWorkflowDiagramNode.ts new file mode 100644 index 000000000..42bf04ba9 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/selectWorkflowDiagramNode.ts @@ -0,0 +1,23 @@ +import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; + +export const selectWorkflowDiagramNode = ({ + diagram, + nodeIdToSelect, +}: { + diagram: T; + nodeIdToSelect: string; +}): T => { + return { + ...diagram, + nodes: diagram.nodes.map((node) => { + if (node.id === nodeIdToSelect) { + return { + ...node, + selected: true, + }; + } + + return node; + }), + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId.ts deleted file mode 100644 index 72a76a5bc..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID = - 'workflow-run-step-side-panel-tab-list';