From 924e599cba8065807ea86696ad329e96fb0337dc Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Wed, 23 Jul 2025 10:30:08 +0200 Subject: [PATCH] Open filters in side panel (#13304) In this PR: - Open filters in the side panel for **workflows** - Open filters in the side panel for **workflow versions** - Preparation for opening filters in the side panel for **workflow runs** - Add many tests to increase the coverage Remaining to do: - Open filters in the side panel for **workflow runs** - Upon filter creation, open it in the side panel --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- ...ItsAViteStaleChunkLazyLoadingError.test.ts | 21 ++ .../isWorkflowRelatedObjectMetadata.test.ts | 50 ++++ .../utils/isWorkflowRelatedObjectMetadata.ts | 2 +- ...kflowRunOpeningInCommandMenuSideEffects.ts | 12 +- .../workflow/hooks/useStepsOutputSchema.ts | 4 +- .../components/WorkflowDiagramBlankEdge.tsx | 31 +++ .../WorkflowDiagramCanvasEditable.tsx | 11 +- .../WorkflowDiagramCanvasReadonly.tsx | 11 +- .../components/WorkflowDiagramDefaultEdge.tsx | 92 ------- .../WorkflowDiagramDefaultEdgeEditable.tsx | 134 +++++++++ .../WorkflowDiagramDefaultEdgeReadonly.tsx | 31 +++ .../WorkflowDiagramDefaultEdgeRun.tsx | 26 ++ .../components/WorkflowDiagramEdgeV1.tsx | 83 ------ .../components/WorkflowDiagramEdgeV2Empty.tsx | 49 ---- .../WorkflowDiagramEdgeV2EmptyContent.tsx | 87 ------ .../WorkflowDiagramEdgeV2Filter.tsx | 60 ---- .../WorkflowDiagramEdgeV2FilterContent.tsx | 220 --------------- .../components/WorkflowDiagramEffect.tsx | 19 +- .../WorkflowDiagramFilterEdgeEditable.tsx | 257 ++++++++++++++++++ .../WorkflowDiagramFilterEdgeReadonly.tsx | 102 +++++++ .../WorkflowDiagramFilterEdgeRun.tsx | 97 +++++++ ...owDiagramFilteringDisabledEdgeEditable.tsx | 108 ++++++++ ...owDiagramFilteringDisabledEdgeReadonly.tsx | 32 +++ ...orkflowDiagramFilteringDisabledEdgeRun.tsx | 27 ++ .../components/WorkflowDiagramSuccessEdge.tsx | 59 ---- .../components/WorkflowRunDiagramBaseEdge.tsx | 65 +++++ .../components/WorkflowRunDiagramCanvas.tsx | 10 +- .../WorkflowRunVisualizerEffect.tsx | 8 + .../WorkflowVersionVisualizerEffect.tsx | 14 +- .../WorkflowDiagramCustomMarkers.stories.tsx | 205 -------------- ...kflowDiagramEdgeV2EmptyContent.stories.tsx | 94 ------- ...flowDiagramEdgeV2FilterContent.stories.tsx | 125 --------- .../WorkflowDiagramSuccessEdgeType.ts | 4 - ...kflowVisualizerEdgeDefaultConfiguration.ts | 7 +- ...kflowVisualizerEdgeSuccessConfiguration.ts | 12 - .../useOpenWorkflowEditFilterInCommandMenu.ts | 65 +++++ .../useOpenWorkflowRunFilterInCommandMenu.ts | 78 ++++++ .../useOpenWorkflowViewFilterInCommandMenu.ts | 72 +++++ .../workflow-diagram/types/WorkflowDiagram.ts | 17 +- .../__tests__/addCreateStepNodes.test.ts | 6 +- .../utils/__tests__/addEdgeOptions.test.ts | 133 --------- .../__tests__/generateWorkflowDiagram.test.ts | 30 +- .../generateWorkflowRunDiagram.test.ts | 89 +++--- .../getWorkflowDiagramTriggerNode.test.ts | 207 ++++++++++++++ .../getWorkflowRunStatusTagProps.test.ts | 82 ++++++ .../getWorkflowVersionDiagram.test.ts | 118 ++++---- .../getWorkflowVersionStatusTagProps.test.ts | 69 +++++ .../transformFilterNodesAsEdges.test.ts | 57 ++-- .../utils/addCreateStepNodes.ts | 2 + .../workflow-diagram/utils/addEdgeOptions.ts | 24 -- .../utils/generateWorkflowDiagram.ts | 6 +- .../utils/generateWorkflowRunDiagram.ts | 55 ++-- .../utils/getWorkflowVersionDiagram.ts | 51 +++- .../utils/transformFilterNodesAsEdges.ts | 20 +- .../getCronTriggerDefaultSettings.test.ts | 9 + .../getManualTriggerDefaultSettings.test.ts | 28 +- .../getTriggerDefaultDefinition.test.ts | 36 +++ .../__tests__/getTriggerStepName.test.ts | 53 ---- .../utils/getTriggerStepName.ts | 34 --- .../utils/__tests__/getWorkspaceUrl.test.ts | 70 +++++ 60 files changed, 2071 insertions(+), 1509 deletions(-) create mode 100644 packages/twenty-front/src/modules/error-handler/utils/__tests__/checkIfItsAViteStaleChunkLazyLoadingError.test.ts create mode 100644 packages/twenty-front/src/modules/object-metadata/utils/__tests__/isWorkflowRelatedObjectMetadata.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBlankEdge.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeReadonly.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeRun.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV1.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Empty.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2EmptyContent.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Filter.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2FilterContent.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeEditable.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeReadonly.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeRun.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeEditable.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeReadonly.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeRun.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramCustomMarkers.stories.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2EmptyContent.stories.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2FilterContent.stories.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowDiagramSuccessEdgeType.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/addEdgeOptions.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowDiagramTriggerNode.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowRunStatusTagProps.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionStatusTagProps.test.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/utils/addEdgeOptions.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getTriggerStepName.test.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/getTriggerStepName.ts create mode 100644 packages/twenty-front/src/utils/__tests__/getWorkspaceUrl.test.ts diff --git a/packages/twenty-front/src/modules/error-handler/utils/__tests__/checkIfItsAViteStaleChunkLazyLoadingError.test.ts b/packages/twenty-front/src/modules/error-handler/utils/__tests__/checkIfItsAViteStaleChunkLazyLoadingError.test.ts new file mode 100644 index 000000000..13879abee --- /dev/null +++ b/packages/twenty-front/src/modules/error-handler/utils/__tests__/checkIfItsAViteStaleChunkLazyLoadingError.test.ts @@ -0,0 +1,21 @@ +import { checkIfItsAViteStaleChunkLazyLoadingError } from '@/error-handler/utils/checkIfItsAViteStaleChunkLazyLoadingError'; + +describe('checkIfItsAViteStaleChunkLazyLoadingError', () => { + it('should return true when error message contains the Vite stale chunk error text', () => { + const error = new Error( + 'Failed to fetch dynamically imported module: /some/module.js', + ); + + const result = checkIfItsAViteStaleChunkLazyLoadingError(error); + + expect(result).toBe(true); + }); + + it('should return false when error message does not contain the Vite stale chunk error text', () => { + const error = new Error('Some other error message'); + + const result = checkIfItsAViteStaleChunkLazyLoadingError(error); + + expect(result).toBe(false); + }); +}); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/isWorkflowRelatedObjectMetadata.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/isWorkflowRelatedObjectMetadata.test.ts new file mode 100644 index 000000000..819551828 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/isWorkflowRelatedObjectMetadata.test.ts @@ -0,0 +1,50 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { isWorkflowRelatedObjectMetadata } from '@/object-metadata/utils/isWorkflowRelatedObjectMetadata'; + +describe('isWorkflowRelatedObjectMetadata', () => { + it('should return true for Workflow object', () => { + const result = isWorkflowRelatedObjectMetadata( + CoreObjectNameSingular.Workflow, + ); + + expect(result).toBe(true); + }); + + it('should return true for WorkflowVersion object', () => { + const result = isWorkflowRelatedObjectMetadata( + CoreObjectNameSingular.WorkflowVersion, + ); + + expect(result).toBe(true); + }); + + it('should return true for WorkflowRun object', () => { + const result = isWorkflowRelatedObjectMetadata( + CoreObjectNameSingular.WorkflowRun, + ); + + expect(result).toBe(true); + }); + + it('should return false for non-workflow related objects', () => { + const result = isWorkflowRelatedObjectMetadata( + CoreObjectNameSingular.Company, + ); + + expect(result).toBe(false); + }); + + it('should return false for unknown object names', () => { + const result = isWorkflowRelatedObjectMetadata('unknownObject'); + + expect(result).toBe(false); + }); + + it('should return false for Person object', () => { + const result = isWorkflowRelatedObjectMetadata( + CoreObjectNameSingular.Person, + ); + + expect(result).toBe(false); + }); +}); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/isWorkflowRelatedObjectMetadata.ts b/packages/twenty-front/src/modules/object-metadata/utils/isWorkflowRelatedObjectMetadata.ts index 77087fd64..2d3bdbe1b 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/isWorkflowRelatedObjectMetadata.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/isWorkflowRelatedObjectMetadata.ts @@ -1,5 +1,5 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata'; -import { CoreObjectNameSingular } from '../types/CoreObjectNameSingular'; export const isWorkflowRelatedObjectMetadata = (objectNameSingular: string) => { return ( diff --git a/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects.ts b/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects.ts index 4e33cc35c..a9f3af0df 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects.ts @@ -14,9 +14,11 @@ import { workflowRunDiagramAutomaticallyOpenedStepsComponentState } from '@/work import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram'; import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; +import { FeatureFlagKey } from '~/generated/graphql'; export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => { const apolloCoreClient = useApolloCoreClient(); @@ -25,6 +27,10 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => { const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const isWorkflowFilteringEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED, + ); + const runWorkflowRunOpeningInCommandMenuSideEffects = useRecoilCallback( ({ snapshot, set }) => ({ @@ -56,6 +62,7 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => { steps: workflowRunRecord.state.flow.steps, stepInfos: workflowRunRecord.state.stepInfos, trigger: workflowRunRecord.state.flow.trigger, + isWorkflowFilteringEnabled, }); if (!isDefined(stepToOpenByDefault)) { @@ -118,9 +125,10 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => { }, [ apolloCoreClient.cache, - getIcon, - openWorkflowRunViewStepInCommandMenu, objectPermissionsByObjectMetadataId, + isWorkflowFilteringEnabled, + openWorkflowRunViewStepInCommandMenu, + getIcon, ], ); diff --git a/packages/twenty-front/src/modules/workflow/hooks/useStepsOutputSchema.ts b/packages/twenty-front/src/modules/workflow/hooks/useStepsOutputSchema.ts index 0feadcb98..2682b91c0 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useStepsOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useStepsOutputSchema.ts @@ -4,11 +4,11 @@ import { getStepOutputSchemaFamilyStateKey } from '@/workflow/utils/getStepOutpu import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId'; import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon'; +import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel'; import { OutputSchema, StepOutputSchema, } from '@/workflow/workflow-variables/types/StepOutputSchema'; -import { getTriggerStepName } from '@/workflow/workflow-variables/utils/getTriggerStepName'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; @@ -41,7 +41,7 @@ export const useStepsOutputSchema = () => { id: TRIGGER_STEP_ID, name: isDefined(trigger.name) ? trigger.name - : getTriggerStepName(trigger), + : getTriggerDefaultLabel(trigger), icon: triggerIconKey, outputSchema: trigger.settings?.outputSchema as OutputSchema, }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBlankEdge.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBlankEdge.tsx new file mode 100644 index 000000000..7c832cd32 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBlankEdge.tsx @@ -0,0 +1,31 @@ +import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth'; +import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { useTheme } from '@emotion/react'; +import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react'; + +type WorkflowDiagramBlankEdgeProps = EdgeProps; + +export const WorkflowDiagramBlankEdge = ({ + markerStart, + markerEnd, + sourceY, + targetY, +}: WorkflowDiagramBlankEdgeProps) => { + const theme = useTheme(); + + const [edgePath] = getStraightPath({ + sourceX: CREATE_STEP_NODE_WIDTH, + sourceY, + targetX: CREATE_STEP_NODE_WIDTH, + targetY, + }); + + return ( + + ); +}; 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 db22a3440..e0b5a172e 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,9 +1,12 @@ import { WorkflowVersionStatus } from '@/workflow/types/Workflow'; +import { WorkflowDiagramBlankEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramBlankEdge'; import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase'; import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect'; import { WorkflowDiagramCreateStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepNode'; -import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge'; +import { WorkflowDiagramDefaultEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable'; import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger'; +import { WorkflowDiagramFilterEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeEditable'; +import { WorkflowDiagramFilteringDisabledEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeEditable'; import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable'; import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps'; import { ReactFlowProvider } from '@xyflow/react'; @@ -26,7 +29,11 @@ export const WorkflowDiagramCanvasEditable = ({ 'empty-trigger': WorkflowDiagramEmptyTrigger, }} edgeTypes={{ - default: WorkflowDiagramDefaultEdge, + blank: WorkflowDiagramBlankEdge, + 'filtering-disabled--editable': + WorkflowDiagramFilteringDisabledEdgeEditable, + 'empty-filter--editable': WorkflowDiagramDefaultEdgeEditable, + 'filter--editable': WorkflowDiagramFilterEdgeEditable, }} tagContainerTestId="workflow-visualizer-status" tagColor={tagProps.color} diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx index cc7e2ec4a..e103e640b 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx @@ -1,10 +1,11 @@ import { WorkflowVersionStatus } from '@/workflow/types/Workflow'; import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase'; import { WorkflowDiagramCanvasReadonlyEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect'; -import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge'; +import { WorkflowDiagramDefaultEdgeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeReadonly'; import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger'; +import { WorkflowDiagramFilterEdgeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeReadonly'; +import { WorkflowDiagramFilteringDisabledEdgeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeReadonly'; import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly'; -import { WorkflowDiagramSuccessEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge'; import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps'; import { ReactFlowProvider } from '@xyflow/react'; @@ -25,8 +26,10 @@ export const WorkflowDiagramCanvasReadonly = ({ 'empty-trigger': WorkflowDiagramEmptyTrigger, }} edgeTypes={{ - default: WorkflowDiagramDefaultEdge, - success: WorkflowDiagramSuccessEdge, + 'filtering-disabled--readonly': + WorkflowDiagramFilteringDisabledEdgeReadonly, + 'empty-filter--readonly': WorkflowDiagramDefaultEdgeReadonly, + 'filter--readonly': WorkflowDiagramFilterEdgeReadonly, }} tagContainerTestId="workflow-visualizer-status" tagColor={tagProps.color} diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge.tsx deleted file mode 100644 index dc5be8aba..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { WorkflowDiagramEdgeV1 } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV1'; -import { WorkflowDiagramEdgeV2Empty } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Empty'; -import { WorkflowDiagramEdgeV2Filter } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Filter'; -import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth'; -import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { useTheme } from '@emotion/react'; -import { - BaseEdge, - EdgeLabelRenderer, - EdgeProps, - getStraightPath, -} from '@xyflow/react'; -import { isDefined } from 'twenty-shared/utils'; -import { FeatureFlagKey } from '~/generated/graphql'; - -type WorkflowDiagramDefaultEdgeProps = EdgeProps; - -export const WorkflowDiagramDefaultEdge = ({ - source, - target, - sourceY, - targetY, - markerStart, - markerEnd, - data, -}: WorkflowDiagramDefaultEdgeProps) => { - const theme = useTheme(); - - const isWorkflowFilteringEnabled = useIsFeatureEnabled( - FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED, - ); - - const [edgePath, labelX, labelY] = getStraightPath({ - sourceX: CREATE_STEP_NODE_WIDTH, - sourceY, - targetX: CREATE_STEP_NODE_WIDTH, - targetY, - }); - - if (!isDefined(data)) { - throw new Error('Edge data is not defined'); - } - - const displayEdgeV1 = !isWorkflowFilteringEnabled && data.isEdgeEditable; - const displayEmptyFilters = - isWorkflowFilteringEnabled && - data.edgeType === 'default' && - data.isEdgeEditable; - const displayFilters = - isWorkflowFilteringEnabled && data.edgeType === 'filter'; - - return ( - <> - - - - {displayEdgeV1 && ( - - )} - {displayEmptyFilters && ( - - )} - {displayFilters && ( - - )} - - - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable.tsx new file mode 100644 index 000000000..49c180345 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable.tsx @@ -0,0 +1,134 @@ +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; +import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined'; +import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container'; +import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer'; +import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth'; +import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId'; +import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; +import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; +import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep'; +import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { + BaseEdge, + EdgeLabelRenderer, + EdgeProps, + getStraightPath, +} from '@xyflow/react'; +import { useState } from 'react'; +import { IconFilter, IconPlus } from 'twenty-ui/display'; +import { IconButtonGroup } from 'twenty-ui/input'; + +type WorkflowDiagramDefaultEdgeEditableProps = EdgeProps; + +const StyledIconButtonGroup = styled(IconButtonGroup)` + pointer-events: all; +`; + +export const WorkflowDiagramDefaultEdgeEditable = ({ + source, + target, + sourceY, + targetY, + markerStart, + markerEnd, +}: WorkflowDiagramDefaultEdgeEditableProps) => { + const theme = useTheme(); + + const [edgePath, labelX, labelY] = getStraightPath({ + sourceX: CREATE_STEP_NODE_WIDTH, + sourceY, + targetX: CREATE_STEP_NODE_WIDTH, + targetY, + }); + + const workflowVisualizerWorkflowId = useRecoilComponentValueV2( + workflowVisualizerWorkflowIdComponentState, + ); + const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId); + assertWorkflowWithCurrentVersionIsDefined(workflow); + + const { createStep } = useCreateStep({ workflow }); + const { startNodeCreation } = useStartNodeCreation(); + + const [hovered, setHovered] = useState(false); + + const workflowInsertStepIds = useRecoilComponentValueV2( + workflowInsertStepIdsComponentState, + ); + + const isSelected = + workflowInsertStepIds.nextStepId === target && + workflowInsertStepIds.parentStepId === source; + + const handleCreateFilter = async () => { + await createStep({ + newStepType: 'FILTER', + parentStepId: source, + nextStepId: target, + }); + + setHovered(false); + }; + + const setWorkflowSelectedNode = useSetRecoilComponentStateV2( + workflowSelectedNodeComponentState, + ); + + const handleFilterButtonClick = () => { + setWorkflowSelectedNode(source); + + handleCreateFilter(); + }; + + const handleNodeButtonClick = () => { + startNodeCreation({ + parentStepId: source, + nextStepId: target, + }); + }; + + return ( + <> + + + + setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeReadonly.tsx new file mode 100644 index 000000000..19fd8458c --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeReadonly.tsx @@ -0,0 +1,31 @@ +import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth'; +import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { useTheme } from '@emotion/react'; +import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react'; + +type WorkflowDiagramDefaultEdgeReadonlyProps = EdgeProps; + +export const WorkflowDiagramDefaultEdgeReadonly = ({ + sourceY, + targetY, + markerStart, + markerEnd, +}: WorkflowDiagramDefaultEdgeReadonlyProps) => { + const theme = useTheme(); + + const [edgePath] = getStraightPath({ + sourceX: CREATE_STEP_NODE_WIDTH, + sourceY, + targetX: CREATE_STEP_NODE_WIDTH, + targetY, + }); + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeRun.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeRun.tsx new file mode 100644 index 000000000..da2b8182c --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeRun.tsx @@ -0,0 +1,26 @@ +import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge'; +import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth'; +import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { EdgeProps, getStraightPath } from '@xyflow/react'; + +type WorkflowDiagramDefaultEdgeRunProps = EdgeProps; + +export const WorkflowDiagramDefaultEdgeRun = ({ + sourceY, + targetY, + data, +}: WorkflowDiagramDefaultEdgeRunProps) => { + const [edgePath] = getStraightPath({ + sourceX: CREATE_STEP_NODE_WIDTH, + sourceY, + targetX: CREATE_STEP_NODE_WIDTH, + targetY, + }); + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV1.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV1.tsx deleted file mode 100644 index 1c48bac3c..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV1.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId'; -import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; -import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState'; -import styled from '@emotion/styled'; -import { useState } from 'react'; -import { IconPlus } from 'twenty-ui/display'; -import { IconButtonGroup } from 'twenty-ui/input'; - -const StyledIconButtonGroup = styled(IconButtonGroup)` - pointer-events: all; -`; - -const StyledContainer = styled.div<{ - labelY?: number; -}>` - position: absolute; - transform: ${({ labelY }) => `translate(${21}px, ${(labelY || 0) - 14}px)`}; -`; - -const StyledHoverZone = styled.div` - position: absolute; - width: 48px; - height: 52px; - transform: translate(-13px, -16px); - background: transparent; -`; - -const StyledWrapper = styled.div` - pointer-events: all; - position: relative; -`; - -type WorkflowDiagramEdgeV1Props = { - labelY?: number; - parentStepId: string; - nextStepId: string; -}; - -export const WorkflowDiagramEdgeV1 = ({ - labelY, - parentStepId, - nextStepId, -}: WorkflowDiagramEdgeV1Props) => { - const [hovered, setHovered] = useState(false); - - const { startNodeCreation } = useStartNodeCreation(); - - const workflowInsertStepIds = useRecoilComponentValueV2( - workflowInsertStepIdsComponentState, - ); - - const isSelected = - workflowInsertStepIds.parentStepId === parentStepId && - workflowInsertStepIds.nextStepId === nextStepId; - - return ( - - setHovered(true)} - onMouseLeave={() => setHovered(false)} - > - - {(hovered || isSelected) && ( - { - startNodeCreation({ parentStepId, nextStepId }); - }, - }, - ]} - /> - )} - - - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Empty.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Empty.tsx deleted file mode 100644 index 169dae830..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Empty.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; -import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined'; -import { WorkflowDiagramEdgeV2EmptyContent } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2EmptyContent'; -import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; -import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep'; - -type WorkflowDiagramEdgeV2EmptyProps = { - labelX: number; - labelY: number; - parentStepId: string; - nextStepId: string; -}; - -export const WorkflowDiagramEdgeV2Empty = ({ - labelX, - labelY, - parentStepId, - nextStepId, -}: WorkflowDiagramEdgeV2EmptyProps) => { - const workflowVisualizerWorkflowId = useRecoilComponentValueV2( - workflowVisualizerWorkflowIdComponentState, - ); - const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId); - assertWorkflowWithCurrentVersionIsDefined(workflow); - - const { createStep } = useCreateStep({ workflow }); - const { startNodeCreation } = useStartNodeCreation(); - - return ( - { - return createStep({ - newStepType: 'FILTER', - parentStepId, - nextStepId, - }); - }} - onCreateNode={() => { - startNodeCreation({ parentStepId, nextStepId }); - }} - /> - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2EmptyContent.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2EmptyContent.tsx deleted file mode 100644 index 402202e26..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2EmptyContent.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; -import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container'; -import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer'; -import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId'; -import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState'; -import styled from '@emotion/styled'; -import { useState } from 'react'; -import { IconFilter, IconPlus } from 'twenty-ui/display'; -import { IconButtonGroup } from 'twenty-ui/input'; - -const StyledIconButtonGroup = styled(IconButtonGroup)` - pointer-events: all; -`; - -type WorkflowDiagramEdgeV2EmptyContentProps = { - labelX: number; - labelY: number; - parentStepId: string; - nextStepId: string; - onCreateFilter: () => Promise; - onCreateNode: () => void; -}; - -export const WorkflowDiagramEdgeV2EmptyContent = ({ - labelX, - labelY, - parentStepId, - nextStepId, - onCreateFilter, - onCreateNode, -}: WorkflowDiagramEdgeV2EmptyContentProps) => { - const [hovered, setHovered] = useState(false); - - const workflowInsertStepIds = useRecoilComponentValueV2( - workflowInsertStepIdsComponentState, - ); - - const isSelected = - workflowInsertStepIds.nextStepId === nextStepId && - workflowInsertStepIds.parentStepId === parentStepId; - - const handleCreateFilter = async () => { - await onCreateFilter(); - - setHovered(false); - }; - - const setWorkflowSelectedNode = useSetRecoilComponentStateV2( - workflowSelectedNodeComponentState, - ); - - const handleFilterButtonClick = () => { - setWorkflowSelectedNode(parentStepId); - - handleCreateFilter(); - }; - - return ( - setHovered(true)} - onMouseLeave={() => setHovered(false)} - > - - - - - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Filter.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Filter.tsx deleted file mode 100644 index 49dee63fc..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Filter.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; -import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { WorkflowDiagramEdgeV2FilterContent } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2FilterContent'; -import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; -import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep'; -import { FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter'; -import { isDefined } from 'twenty-shared/utils'; - -type WorkflowDiagramEdgeV2FilterProps = { - labelX: number; - labelY: number; - stepId: string; - parentStepId: string; - nextStepId: string; - filterSettings: FilterSettings; - isEdgeEditable: boolean; -}; - -export const WorkflowDiagramEdgeV2Filter = ({ - labelX, - labelY, - stepId, - parentStepId, - nextStepId, - filterSettings, - isEdgeEditable, -}: WorkflowDiagramEdgeV2FilterProps) => { - const workflowVisualizerWorkflowId = useRecoilComponentValueV2( - workflowVisualizerWorkflowIdComponentState, - ); - const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId); - - const { deleteStep } = useDeleteStep({ workflow }); - const { startNodeCreation } = useStartNodeCreation(); - - return ( - { - if (!isDefined(stepId)) { - throw new Error( - 'Step ID must be configured for the edge when rendering a filter', - ); - } - - return deleteStep(stepId); - }} - onCreateNode={() => { - startNodeCreation({ parentStepId: stepId, nextStepId }); - }} - /> - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2FilterContent.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2FilterContent.tsx deleted file mode 100644 index 413458985..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2FilterContent.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; -import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; -import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; -import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; -import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; -import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown'; -import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; -import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container'; -import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer'; -import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId'; -import { workflowDiagramPanOnDragComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramPanOnDragComponentState'; -import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState'; -import { FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter'; -import styled from '@emotion/styled'; -import { isNonEmptyString } from '@sniptt/guards'; -import { useState } from 'react'; -import { isDefined } from 'twenty-shared/utils'; -import { - IconDotsVertical, - IconFilter, - IconFilterX, - IconPlus, -} from 'twenty-ui/display'; -import { IconButtonGroup } from 'twenty-ui/input'; -import { MenuItem } from 'twenty-ui/navigation'; - -const StyledIconButtonGroup = styled(IconButtonGroup)` - pointer-events: all; -`; - -const StyledConfiguredFilterContainer = styled.div` - height: 26px; - width: 26px; -`; - -type WorkflowDiagramEdgeV2FilterContentProps = { - labelX: number; - labelY: number; - stepId: string; - parentStepId: string; - nextStepId: string; - filterSettings: FilterSettings; - onDeleteFilter: () => Promise; - onCreateNode: () => void; - isEdgeEditable: boolean; -}; - -export const WorkflowDiagramEdgeV2FilterContent = ({ - labelX, - labelY, - stepId, - parentStepId, - nextStepId, - onDeleteFilter, - onCreateNode, - isEdgeEditable, -}: WorkflowDiagramEdgeV2FilterContentProps) => { - const { openDropdown } = useOpenDropdown(); - const { closeDropdown } = useCloseDropdown(); - - const [hovered, setHovered] = useState(false); - - const setWorkflowDiagramPanOnDrag = useSetRecoilComponentStateV2( - workflowDiagramPanOnDragComponentState, - ); - - const workflowInsertStepIds = useRecoilComponentValueV2( - workflowInsertStepIdsComponentState, - ); - - const isSelected = - workflowInsertStepIds.nextStepId === nextStepId && - (workflowInsertStepIds.parentStepId === parentStepId || - (isNonEmptyString(stepId) && - workflowInsertStepIds.parentStepId === stepId)); - - const dropdownId = `${WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}-${parentStepId}-${nextStepId}`; - - const isDropdownOpen = useRecoilComponentValueV2( - isDropdownOpenComponentState, - dropdownId, - ); - - const workflowVisualizerWorkflowId = useRecoilComponentValueV2( - workflowVisualizerWorkflowIdComponentState, - ); - - const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu(); - - const setWorkflowSelectedNode = useSetRecoilComponentStateV2( - workflowSelectedNodeComponentState, - ); - - const handleMouseEnter = () => { - if (!isEdgeEditable) { - return; - } - - setHovered(true); - }; - - const handleMouseLeave = () => { - setHovered(false); - }; - - const handleFilterButtonClick = () => { - setWorkflowSelectedNode(stepId); - - if (isDefined(workflowVisualizerWorkflowId)) { - openWorkflowEditStepInCommandMenu( - workflowVisualizerWorkflowId, - 'Filter', - IconFilter, - ); - } - }; - - return ( - - - - {hovered || isDropdownOpen || isSelected ? ( - { - openDropdown({ - dropdownComponentInstanceIdFromProps: dropdownId, - }); - }, - }, - ]} - /> - ) : ( - - )} - - - } - data-select-disable - dropdownPlacement="bottom-start" - dropdownStrategy="absolute" - dropdownOffset={{ - x: 24, - y: 4, - }} - onOpen={() => { - setWorkflowDiagramPanOnDrag(false); - }} - onClose={() => { - setWorkflowDiagramPanOnDrag(true); - }} - dropdownComponents={ - - - { - closeDropdown(dropdownId); - setHovered(false); - - handleFilterButtonClick(); - }} - /> - { - closeDropdown(dropdownId); - setHovered(false); - - onDeleteFilter(); - }} - /> - { - closeDropdown(dropdownId); - setHovered(false); - - onCreateNode(); - }} - /> - - - } - /> - - - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx index 3efff1f38..65af04fb8 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx @@ -13,10 +13,11 @@ import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/state import { addCreateStepNodes } from '@/workflow/workflow-diagram/utils/addCreateStepNodes'; import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram'; import { mergeWorkflowDiagrams } from '@/workflow/workflow-diagram/utils/mergeWorkflowDiagrams'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useEffect } from 'react'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; -import { addEdgeOptions } from '@/workflow/workflow-diagram/utils/addEdgeOptions'; +import { FeatureFlagKey } from '~/generated/graphql'; export const WorkflowDiagramEffect = ({ workflowWithCurrentVersion, @@ -36,6 +37,10 @@ export const WorkflowDiagramEffect = ({ workflowLastCreatedStepIdComponentState, ); + const isWorkflowFilteringEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED, + ); + const computeAndMergeNewWorkflowDiagram = useRecoilCallback( ({ snapshot, set }) => { return (currentVersion: WorkflowVersion) => { @@ -45,7 +50,11 @@ export const WorkflowDiagramEffect = ({ ); const nextWorkflowDiagram = addCreateStepNodes( - addEdgeOptions(getWorkflowVersionDiagram(currentVersion)), + getWorkflowVersionDiagram({ + workflowVersion: currentVersion, + isWorkflowFilteringEnabled, + isEditable: true, + }), ); let mergedWorkflowDiagram = nextWorkflowDiagram; @@ -78,7 +87,11 @@ export const WorkflowDiagramEffect = ({ set(workflowDiagramState, mergedWorkflowDiagram); }; }, - [workflowLastCreatedStepIdState, workflowDiagramState], + [ + workflowDiagramState, + isWorkflowFilteringEnabled, + workflowLastCreatedStepIdState, + ], ); const currentVersion = workflowWithCurrentVersion?.currentVersion; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeEditable.tsx new file mode 100644 index 000000000..1b6f74f3c --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeEditable.tsx @@ -0,0 +1,257 @@ +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; +import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; +import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown'; +import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; +import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container'; +import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer'; +import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth'; +import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId'; +import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu'; +import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; +import { workflowDiagramPanOnDragComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramPanOnDragComponentState'; +import { + WorkflowDiagramEdge, + WorkflowDiagramEdgeData, +} from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep'; +import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { isNonEmptyString } from '@sniptt/guards'; +import { + BaseEdge, + EdgeLabelRenderer, + EdgeProps, + getStraightPath, +} from '@xyflow/react'; +import { useState } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { + IconDotsVertical, + IconFilter, + IconFilterX, + IconPlus, +} from 'twenty-ui/display'; +import { IconButtonGroup } from 'twenty-ui/input'; +import { MenuItem } from 'twenty-ui/navigation'; + +type WorkflowDiagramFilterEdgeEditableProps = EdgeProps; + +const assertFilterEdgeDataOrThrow: ( + data: WorkflowDiagramEdgeData | undefined, +) => asserts data is WorkflowDiagramEdgeData & { edgeType: 'filter' } = ( + data: WorkflowDiagramEdgeData | undefined, +) => { + if (data?.edgeType !== 'filter') { + throw new Error('Edge data must be of type "filter"'); + } +}; + +const StyledIconButtonGroup = styled(IconButtonGroup)` + pointer-events: all; +`; + +const StyledConfiguredFilterContainer = styled.div` + height: 26px; + width: 26px; +`; + +export const WorkflowDiagramFilterEdgeEditable = ({ + source, + target, + sourceY, + targetY, + markerStart, + markerEnd, + data, +}: WorkflowDiagramFilterEdgeEditableProps) => { + assertFilterEdgeDataOrThrow(data); + + const theme = useTheme(); + + const [edgePath, labelX, labelY] = getStraightPath({ + sourceX: CREATE_STEP_NODE_WIDTH, + sourceY, + targetX: CREATE_STEP_NODE_WIDTH, + targetY, + }); + + const workflowVisualizerWorkflowId = useRecoilComponentValueV2( + workflowVisualizerWorkflowIdComponentState, + ); + const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId); + + const { deleteStep } = useDeleteStep({ workflow }); + const { startNodeCreation } = useStartNodeCreation(); + + const { openDropdown } = useOpenDropdown(); + const { closeDropdown } = useCloseDropdown(); + + const [hovered, setHovered] = useState(false); + + const setWorkflowDiagramPanOnDrag = useSetRecoilComponentStateV2( + workflowDiagramPanOnDragComponentState, + ); + + const workflowInsertStepIds = useRecoilComponentValueV2( + workflowInsertStepIdsComponentState, + ); + + const isSelected = + workflowInsertStepIds.nextStepId === source && + (workflowInsertStepIds.parentStepId === target || + (isNonEmptyString(data.stepId) && + workflowInsertStepIds.parentStepId === data.stepId)); + + const dropdownId = `${WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}-${source}-${target}`; + + const isDropdownOpen = useRecoilComponentValueV2( + isDropdownOpenComponentState, + dropdownId, + ); + + const { openWorkflowEditFilterInCommandMenu } = + useOpenWorkflowEditFilterInCommandMenu(); + + const handleMouseEnter = () => { + setHovered(true); + }; + + const handleMouseLeave = () => { + setHovered(false); + }; + + const handleFilterButtonClick = () => { + openWorkflowEditFilterInCommandMenu({ + stepId: data.stepId, + stepName: data.name, + }); + }; + + return ( + <> + + + + + + + {hovered || isDropdownOpen || isSelected ? ( + { + openDropdown({ + dropdownComponentInstanceIdFromProps: dropdownId, + }); + }, + }, + ]} + /> + ) : ( + + )} + + + } + data-select-disable + dropdownPlacement="bottom-start" + dropdownStrategy="absolute" + dropdownOffset={{ + x: 24, + y: 4, + }} + onOpen={() => { + setWorkflowDiagramPanOnDrag(false); + }} + onClose={() => { + setWorkflowDiagramPanOnDrag(true); + }} + dropdownComponents={ + + + { + closeDropdown(dropdownId); + setHovered(false); + + handleFilterButtonClick(); + }} + /> + { + closeDropdown(dropdownId); + setHovered(false); + + if (!isDefined(data.stepId)) { + throw new Error( + 'Step ID must be configured for the edge when rendering a filter', + ); + } + + return deleteStep(data.stepId); + }} + /> + { + closeDropdown(dropdownId); + setHovered(false); + + startNodeCreation({ + parentStepId: data.stepId, + nextStepId: target, + }); + }} + /> + + + } + /> + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeReadonly.tsx new file mode 100644 index 000000000..e089acf85 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeReadonly.tsx @@ -0,0 +1,102 @@ +import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container'; +import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer'; +import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth'; +import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId'; +import { useOpenWorkflowViewFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu'; +import { + WorkflowDiagramEdge, + WorkflowDiagramEdgeData, +} from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { + BaseEdge, + EdgeLabelRenderer, + EdgeProps, + getStraightPath, +} from '@xyflow/react'; +import { IconFilter } from 'twenty-ui/display'; +import { IconButtonGroup } from 'twenty-ui/input'; + +type WorkflowDiagramFilterEdgeReadonlyProps = EdgeProps; + +const assertFilterEdgeDataOrThrow: ( + data: WorkflowDiagramEdgeData | undefined, +) => asserts data is WorkflowDiagramEdgeData & { edgeType: 'filter' } = ( + data: WorkflowDiagramEdgeData | undefined, +) => { + if (data?.edgeType !== 'filter') { + throw new Error('Edge data must be of type "filter"'); + } +}; + +const StyledIconButtonGroup = styled(IconButtonGroup)` + pointer-events: all; +`; + +const StyledConfiguredFilterContainer = styled.div` + height: 26px; + width: 26px; +`; + +export const WorkflowDiagramFilterEdgeReadonly = ({ + sourceY, + targetY, + markerStart, + markerEnd, + data, +}: WorkflowDiagramFilterEdgeReadonlyProps) => { + assertFilterEdgeDataOrThrow(data); + + const theme = useTheme(); + + const [edgePath, labelX, labelY] = getStraightPath({ + sourceX: CREATE_STEP_NODE_WIDTH, + sourceY, + targetX: CREATE_STEP_NODE_WIDTH, + targetY, + }); + + const { openWorkflowViewFilterInCommandMenu } = + useOpenWorkflowViewFilterInCommandMenu(); + + const handleFilterButtonClick = () => { + openWorkflowViewFilterInCommandMenu({ + stepId: data.stepId, + stepName: data.name, + }); + }; + + return ( + <> + + + + + + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeRun.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeRun.tsx new file mode 100644 index 000000000..95db0dbf3 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeRun.tsx @@ -0,0 +1,97 @@ +import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container'; +import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer'; +import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge'; +import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth'; +import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId'; +import { useOpenWorkflowRunFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu'; +import { + WorkflowDiagramEdge, + WorkflowDiagramEdgeData, +} from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import styled from '@emotion/styled'; +import { EdgeLabelRenderer, EdgeProps, getStraightPath } from '@xyflow/react'; +import { isDefined } from 'twenty-shared/utils'; +import { IconFilter } from 'twenty-ui/display'; +import { IconButtonGroup } from 'twenty-ui/input'; + +type WorkflowDiagramFilterEdgeRunProps = EdgeProps; + +const assertFilterEdgeDataOrThrow: ( + data: WorkflowDiagramEdgeData | undefined, +) => asserts data is WorkflowDiagramEdgeData & { edgeType: 'filter' } = ( + data: WorkflowDiagramEdgeData | undefined, +) => { + if (data?.edgeType !== 'filter') { + throw new Error('Edge data must be of type "filter"'); + } +}; + +const StyledIconButtonGroup = styled(IconButtonGroup)` + pointer-events: all; +`; + +const StyledConfiguredFilterContainer = styled.div` + height: 26px; + width: 26px; +`; + +export const WorkflowDiagramFilterEdgeRun = ({ + sourceY, + targetY, + data, +}: WorkflowDiagramFilterEdgeRunProps) => { + assertFilterEdgeDataOrThrow(data); + + const [edgePath, labelX, labelY] = getStraightPath({ + sourceX: CREATE_STEP_NODE_WIDTH, + sourceY, + targetX: CREATE_STEP_NODE_WIDTH, + targetY, + }); + + const { openWorkflowRunFilterInCommandMenu } = + useOpenWorkflowRunFilterInCommandMenu(); + + const handleFilterButtonClick = () => { + if (!isDefined(data.runStatus)) { + throw new Error('Run status must be set on edge data for workflow runs'); + } + + openWorkflowRunFilterInCommandMenu({ + stepId: data.stepId, + stepName: data.name, + stepExecutionStatus: data.runStatus, + }); + }; + + return ( + <> + + + + + + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeEditable.tsx new file mode 100644 index 000000000..14f33010b --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeEditable.tsx @@ -0,0 +1,108 @@ +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth'; +import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId'; +import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; +import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react'; +import { useState } from 'react'; +import { IconPlus } from 'twenty-ui/display'; +import { IconButtonGroup } from 'twenty-ui/input'; + +const StyledIconButtonGroup = styled(IconButtonGroup)` + pointer-events: all; +`; + +const StyledContainer = styled.div<{ + labelY?: number; +}>` + position: absolute; + transform: ${({ labelY }) => `translate(${21}px, ${(labelY || 0) - 14}px)`}; +`; + +const StyledHoverZone = styled.div` + position: absolute; + width: 48px; + height: 52px; + transform: translate(-13px, -16px); + background: transparent; +`; + +const StyledWrapper = styled.div` + pointer-events: all; + position: relative; +`; + +type WorkflowDiagramFilteringDisabledEdgeEditableProps = + EdgeProps; + +export const WorkflowDiagramFilteringDisabledEdgeEditable = ({ + markerStart, + markerEnd, + source, + sourceY, + target, + targetY, +}: WorkflowDiagramFilteringDisabledEdgeEditableProps) => { + const theme = useTheme(); + + const [edgePath, , labelY] = getStraightPath({ + sourceX: CREATE_STEP_NODE_WIDTH, + sourceY, + targetX: CREATE_STEP_NODE_WIDTH, + targetY, + }); + + const [hovered, setHovered] = useState(false); + + const { startNodeCreation } = useStartNodeCreation(); + + const workflowInsertStepIds = useRecoilComponentValueV2( + workflowInsertStepIdsComponentState, + ); + + const isSelected = + workflowInsertStepIds.parentStepId === source && + workflowInsertStepIds.nextStepId === target; + + return ( + <> + + + + setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + + {(hovered || isSelected) && ( + { + startNodeCreation({ + parentStepId: source, + nextStepId: target, + }); + }, + }, + ]} + /> + )} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeReadonly.tsx new file mode 100644 index 000000000..79f228403 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeReadonly.tsx @@ -0,0 +1,32 @@ +import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth'; +import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { useTheme } from '@emotion/react'; +import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react'; + +type WorkflowDiagramFilteringDisabledEdgeReadonlyProps = + EdgeProps; + +export const WorkflowDiagramFilteringDisabledEdgeReadonly = ({ + markerStart, + markerEnd, + sourceY, + targetY, +}: WorkflowDiagramFilteringDisabledEdgeReadonlyProps) => { + const theme = useTheme(); + + const [edgePath] = getStraightPath({ + sourceX: CREATE_STEP_NODE_WIDTH, + sourceY, + targetX: CREATE_STEP_NODE_WIDTH, + targetY, + }); + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeRun.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeRun.tsx new file mode 100644 index 000000000..be691c9aa --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeRun.tsx @@ -0,0 +1,27 @@ +import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge'; +import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth'; +import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { EdgeProps, getStraightPath } from '@xyflow/react'; + +type WorkflowDiagramFilteringDisabledEdgeRunProps = + EdgeProps; + +export const WorkflowDiagramFilteringDisabledEdgeRun = ({ + sourceY, + targetY, + data, +}: WorkflowDiagramFilteringDisabledEdgeRunProps) => { + const [edgePath] = getStraightPath({ + sourceX: CREATE_STEP_NODE_WIDTH, + sourceY, + targetX: CREATE_STEP_NODE_WIDTH, + targetY, + }); + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge.tsx deleted file mode 100644 index d9b56943f..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { EDGE_GREEN_ROUNDED_ARROW_MARKER_WIDTH_PX } from '@/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerWidthPx'; -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { - BaseEdge, - EdgeLabelRenderer, - EdgeProps, - getStraightPath, -} from '@xyflow/react'; -import { Label } from 'twenty-ui/display'; -import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth'; - -const StyledLabel = styled(Label)` - color: ${({ theme }) => theme.tag.text.turquoise}; -`; - -type WorkflowDiagramSuccessEdgeProps = EdgeProps; - -export const WorkflowDiagramSuccessEdge = ({ - sourceY, - targetY, - markerStart, - markerEnd, - label, -}: WorkflowDiagramSuccessEdgeProps) => { - const theme = useTheme(); - - const [edgePath, labelX, labelY] = getStraightPath({ - sourceX: CREATE_STEP_NODE_WIDTH, - sourceY, - targetX: CREATE_STEP_NODE_WIDTH, - targetY, - }); - - return ( - <> - - - - - {label} - - - - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge.tsx new file mode 100644 index 000000000..e8a8b4dcf --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge.tsx @@ -0,0 +1,65 @@ +import { EDGE_GRAY_CIRCLE_MARKED_ID } from '@/workflow/workflow-diagram/constants/EdgeGrayCircleMarkedId'; +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 { EDGE_ROUNDED_ARROW_MARKER_ID } from '@/workflow/workflow-diagram/constants/EdgeRoundedArrowMarkerId'; +import { Theme, useTheme } from '@emotion/react'; +import { BaseEdge } from '@xyflow/react'; +import { StepStatus } from 'twenty-shared/workflow'; + +const toMarkerId = (id: string) => `url(#${id})`; + +const getMarkerStart = (edgeExecutionStatus: StepStatus | undefined) => { + if (edgeExecutionStatus === StepStatus.SUCCESS) { + return EDGE_GREEN_CIRCLE_MARKED_ID; + } + + return EDGE_GRAY_CIRCLE_MARKED_ID; +}; + +const getMarkerEnd = (edgeExecutionStatus: StepStatus | undefined) => { + if (edgeExecutionStatus === StepStatus.SUCCESS) { + return EDGE_GREEN_ROUNDED_ARROW_MARKER_ID; + } + + return EDGE_ROUNDED_ARROW_MARKER_ID; +}; + +const getStrokeColor = ({ + theme, + edgeExecutionStatus, +}: { + theme: Theme; + edgeExecutionStatus: StepStatus | undefined; +}) => { + if (edgeExecutionStatus === StepStatus.SUCCESS) { + return theme.tag.text.turquoise; + } + + return theme.border.color.strong; +}; + +type WorkflowRunDiagramBaseEdgeProps = { + edgePath: string; + edgeExecutionStatus: StepStatus | undefined; +}; + +export const WorkflowRunDiagramBaseEdge = ({ + edgePath, + edgeExecutionStatus, +}: WorkflowRunDiagramBaseEdgeProps) => { + const theme = useTheme(); + + return ( + + ); +}; 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 1d3006d67..ecb37618b 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 @@ -1,8 +1,9 @@ import { WorkflowRunStatus } from '@/workflow/types/Workflow'; import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase'; -import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge'; +import { WorkflowDiagramDefaultEdgeRun } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeRun'; +import { WorkflowDiagramFilterEdgeRun } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeRun'; +import { WorkflowDiagramFilteringDisabledEdgeRun } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeRun'; 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'; @@ -27,8 +28,9 @@ export const WorkflowRunDiagramCanvas = ({ default: WorkflowDiagramStepNodeReadonly, }} edgeTypes={{ - default: WorkflowDiagramDefaultEdge, - success: WorkflowDiagramSuccessEdge, + 'filtering-disabled--run': WorkflowDiagramFilteringDisabledEdgeRun, + 'empty-filter--run': WorkflowDiagramDefaultEdgeRun, + 'filter--run': WorkflowDiagramFilterEdgeRun, }} tagContainerTestId="workflow-run-status" tagColor={tagProps.color} 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 5c2ff81f5..91516e265 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 @@ -18,10 +18,12 @@ import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/ import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram'; import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; import { selectWorkflowDiagramNode } from '@/workflow/workflow-diagram/utils/selectWorkflowDiagramNode'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useContext, useEffect } from 'react'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; +import { FeatureFlagKey } from '~/generated/graphql'; export const WorkflowRunVisualizerEffect = ({ workflowRunId, @@ -67,6 +69,10 @@ export const WorkflowRunVisualizerEffect = ({ const { isInRightDrawer } = useContext(ActionMenuContext); + const isWorkflowFilteringEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED, + ); + useEffect(() => { setWorkflowRunId(workflowRunId); }, [setWorkflowRunId, workflowRunId]); @@ -117,6 +123,7 @@ export const WorkflowRunVisualizerEffect = ({ trigger: workflowRunState.flow.trigger, steps: workflowRunState.flow.steps, stepInfos: workflowRunState.stepInfos, + isWorkflowFilteringEnabled, }); if (isDefined(stepToOpenByDefault)) { @@ -184,6 +191,7 @@ export const WorkflowRunVisualizerEffect = ({ [ flowState, getIcon, + isWorkflowFilteringEnabled, openWorkflowRunViewStepInCommandMenu, workflowDiagramState, workflowDiagramStatusState, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx index 084fc5037..1a0ccaf6e 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx @@ -6,8 +6,10 @@ import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/wo import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState'; import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useEffect } from 'react'; import { isDefined } from 'twenty-shared/utils'; +import { FeatureFlagKey } from '~/generated/graphql'; export const WorkflowVersionVisualizerEffect = ({ workflowVersionId, @@ -29,6 +31,10 @@ export const WorkflowVersionVisualizerEffect = ({ const { populateStepsOutputSchema } = useStepsOutputSchema(); + const isWorkflowFilteringEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED, + ); + useEffect(() => { if (!isDefined(workflowVersion)) { setFlow(undefined); @@ -58,10 +64,14 @@ export const WorkflowVersionVisualizerEffect = ({ return; } - const nextWorkflowDiagram = getWorkflowVersionDiagram(workflowVersion); + const nextWorkflowDiagram = getWorkflowVersionDiagram({ + workflowVersion, + isWorkflowFilteringEnabled, + isEditable: false, + }); setWorkflowDiagram(nextWorkflowDiagram); - }, [setWorkflowDiagram, workflowVersion]); + }, [isWorkflowFilteringEnabled, setWorkflowDiagram, workflowVersion]); useEffect(() => { if (!isDefined(workflowVersion)) { diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramCustomMarkers.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramCustomMarkers.stories.tsx deleted file mode 100644 index 8a5eec03c..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramCustomMarkers.stories.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import styled from '@emotion/styled'; -import { Meta, StoryObj } from '@storybook/react'; -import { RecoilRoot } from 'recoil'; - -import { WorkflowDiagramCreateStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepNode'; -import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge'; -import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger'; -import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly'; -import { WorkflowDiagramSuccessEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge'; -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 { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; -import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; -import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator'; -import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator'; -import { graphqlMocks } from '~/testing/graphqlMocks'; -import { workflowDiagramComponentState } from '../../states/workflowDiagramComponentState'; -import { WorkflowDiagramCanvasBase } from '../WorkflowDiagramCanvasBase'; - -const StyledContainer = styled.div` - height: 400px; - width: 100%; - position: relative; -`; - -const meta: Meta = { - title: 'Modules/Workflow/WorkflowDiagram/WorkflowDiagramCustomMarkers', - component: WorkflowDiagramCanvasBase, - parameters: { - msw: graphqlMocks, - }, - decorators: [ - WorkspaceDecorator, - ObjectMetadataItemsDecorator, - ReactflowDecorator, - ], -}; - -export default meta; - -type Story = StoryObj; - -export const DefaultEdge: Story = { - args: { - nodeTypes: { - default: WorkflowDiagramStepNodeReadonly, - 'create-step': WorkflowDiagramCreateStepNode, - 'empty-trigger': WorkflowDiagramEmptyTrigger, - }, - edgeTypes: { - default: WorkflowDiagramDefaultEdge, - }, - }, - decorators: [ - (Story) => { - const workflowVisualizerComponentInstanceId = - 'workflow-visualizer-test-id'; - - return ( - { - set( - workflowDiagramComponentState.atomFamily({ - instanceId: workflowVisualizerComponentInstanceId, - }), - { - nodes: [ - { - id: 'trigger-1', - type: 'default', - position: { x: 100, y: 100 }, - data: { - nodeType: 'trigger', - triggerType: 'DATABASE_EVENT', - name: 'When record is created', - }, - }, - { - id: 'action-1', - type: 'default', - position: { x: 300, y: 100 }, - data: { - nodeType: 'action', - actionType: 'CREATE_RECORD', - name: 'Create record', - }, - }, - { - id: 'create-step-1', - type: 'create-step', - position: { x: 500, y: 100 }, - data: { - nodeType: 'create-step', - parentNodeId: 'action-1', - }, - }, - ], - edges: [ - { - ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION, - id: 'edge-1', - source: 'trigger-1', - target: 'action-1', - }, - { - ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION, - id: 'edge-2', - source: 'action-1', - target: 'create-step-1', - }, - ], - }, - ); - }} - > - - - - - - - ); - }, - ], -}; - -export const SuccessEdge: Story = { - args: { - nodeTypes: { - default: WorkflowDiagramStepNodeReadonly, - 'create-step': WorkflowDiagramCreateStepNode, - 'empty-trigger': WorkflowDiagramEmptyTrigger, - }, - edgeTypes: { - default: WorkflowDiagramDefaultEdge, - success: WorkflowDiagramSuccessEdge, - }, - }, - decorators: [ - (Story) => { - const workflowVisualizerComponentInstanceId = - 'workflow-visualizer-test-id'; - - return ( - { - set( - workflowDiagramComponentState.atomFamily({ - instanceId: workflowVisualizerComponentInstanceId, - }), - { - nodes: [ - { - id: 'trigger-1', - type: 'default', - position: { x: 100, y: 100 }, - data: { - nodeType: 'trigger', - triggerType: 'DATABASE_EVENT', - name: 'When record is created', - }, - }, - { - id: 'action-1', - type: 'default', - position: { x: 300, y: 100 }, - data: { - nodeType: 'action', - actionType: 'CREATE_RECORD', - name: 'Create record', - }, - }, - ], - edges: [ - { - ...WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION, - id: 'edge-1', - source: 'trigger-1', - target: 'action-1', - type: 'success', - label: '1 item', - }, - ], - }, - ); - }} - > - - - - - - - ); - }, - ], -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2EmptyContent.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2EmptyContent.stories.tsx deleted file mode 100644 index 5a12f537d..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2EmptyContent.stories.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; -import { Meta, StoryObj } from '@storybook/react'; -import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; -import '@xyflow/react/dist/style.css'; -import { ComponentDecorator } from 'twenty-ui/testing'; -import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator'; -import { WorkflowDiagramEdgeV2EmptyContent } from '../WorkflowDiagramEdgeV2EmptyContent'; - -const meta: Meta = { - title: 'Modules/Workflow/WorkflowDiagramEdgeV2EmptyContent', - component: WorkflowDiagramEdgeV2EmptyContent, - decorators: [ - ComponentDecorator, - ReactflowDecorator, - (Story) => { - const workflowVisualizerComponentInstanceId = - 'workflow-visualizer-test-id'; - - return ( - - - - ); - }, - ], - args: { - labelX: 0, - labelY: 0, - parentStepId: 'parent-step-id', - nextStepId: 'next-step-id', - onCreateFilter: fn(), - onCreateNode: fn(), - }, -}; - -export default meta; -type Story = StoryObj; - -export const ButtonsAppearOnHover: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const buttons = await canvas.findAllByRole('button'); - const filterButton = buttons[0]; - - userEvent.hover(filterButton); - - await waitFor(() => { - expect(filterButton).toBeVisible(); - }); - }, -}; - -export const CreateFilter: Story = { - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const buttons = await canvas.findAllByRole('button'); - const filterButton = buttons[0]; - - userEvent.hover(filterButton); - - await waitFor(() => { - expect(filterButton).toBeVisible(); - }); - - userEvent.click(filterButton); - - await waitFor(() => { - expect(args.onCreateFilter).toHaveBeenCalledTimes(1); - }); - }, -}; - -export const AddNodeAction: Story = { - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const buttons = await canvas.findAllByRole('button'); - const addNodeButton = buttons[1]; - - userEvent.hover(addNodeButton); - - userEvent.click(addNodeButton); - - await waitFor(() => { - expect(args.onCreateNode).toHaveBeenCalledTimes(1); - }); - }, -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2FilterContent.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2FilterContent.stories.tsx deleted file mode 100644 index 480ef303f..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2FilterContent.stories.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; -import { Meta, StoryObj } from '@storybook/react'; -import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; -import '@xyflow/react/dist/style.css'; -import { - ComponentDecorator, - getCanvasElementForDropdownTesting, -} from 'twenty-ui/testing'; -import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator'; -import { WorkflowDiagramEdgeV2FilterContent } from '../WorkflowDiagramEdgeV2FilterContent'; - -const meta: Meta = { - title: 'Modules/Workflow/WorkflowDiagramEdgeV2FilterContent', - component: WorkflowDiagramEdgeV2FilterContent, - decorators: [ - ComponentDecorator, - ReactflowDecorator, - (Story) => { - const workflowVisualizerComponentInstanceId = - 'workflow-visualizer-test-id'; - - return ( - - - - ); - }, - ], - args: { - labelX: 0, - labelY: 0, - parentStepId: 'parent-step-id', - nextStepId: 'next-step-id', - onDeleteFilter: fn(), - onCreateNode: fn(), - }, -}; - -export default meta; -type Story = StoryObj; - -export const ButtonsAppearOnHover: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const buttons = await canvas.findAllByRole('button'); - const filterButton = buttons[0]; - - userEvent.hover(filterButton); - - await waitFor(() => { - expect(filterButton).toBeVisible(); - }); - }, -}; - -export const AddNodeAction: Story = { - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const buttons = await canvas.findAllByRole('button'); - const dotsButton = buttons[1]; - - userEvent.hover(dotsButton); - - await waitFor(() => { - expect(dotsButton).toBeVisible(); - }); - - userEvent.click(dotsButton); - - const addNodeButton = await within( - getCanvasElementForDropdownTesting(), - ).findByText('Add Node'); - - userEvent.click(addNodeButton); - - await waitFor(() => { - expect(canvas.queryByText('Add Node')).not.toBeInTheDocument(); - }); - - await waitFor(() => { - expect(args.onCreateNode).toHaveBeenCalledTimes(1); - }); - }, -}; - -export const DropdownInteractions: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const buttons = await canvas.findAllByRole('button'); - const dotsButton = buttons[1]; - - userEvent.hover(dotsButton); - - await waitFor(() => { - expect(dotsButton).toBeVisible(); - }); - - userEvent.click(dotsButton); - - const dropdownCanvas = within(getCanvasElementForDropdownTesting()); - - await waitFor(() => { - expect(dropdownCanvas.getByText('Filter')).toBeVisible(); - }); - - userEvent.click(canvasElement); - - await waitFor(() => { - expect(dropdownCanvas.queryByText('Filter')).not.toBeInTheDocument(); - }); - - userEvent.click(dotsButton); - - await waitFor(() => { - expect(dropdownCanvas.getByText('Filter')).toBeVisible(); - }); - }, -}; 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 deleted file mode 100644 index 0ba1f2b45..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowDiagramSuccessEdgeType.ts +++ /dev/null @@ -1,4 +0,0 @@ -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/WorkflowVisualizerEdgeDefaultConfiguration.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeDefaultConfiguration.ts index 3b8786598..2c0d7cc22 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeDefaultConfiguration.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeDefaultConfiguration.ts @@ -1,14 +1,17 @@ import { EDGE_GRAY_CIRCLE_MARKED_ID } from '@/workflow/workflow-diagram/constants/EdgeGrayCircleMarkedId'; import { EDGE_ROUNDED_ARROW_MARKER_ID } from '@/workflow/workflow-diagram/constants/EdgeRoundedArrowMarkerId'; -import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { + WorkflowDiagramEdge, + WorkflowDiagramEdgeType, +} from '@/workflow/workflow-diagram/types/WorkflowDiagram'; export const WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION = { + type: 'empty-filter--readonly' satisfies WorkflowDiagramEdgeType, markerStart: EDGE_GRAY_CIRCLE_MARKED_ID, markerEnd: EDGE_ROUNDED_ARROW_MARKER_ID, deletable: false, selectable: false, data: { edgeType: 'default', - isEdgeEditable: false, }, } satisfies Partial; 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 deleted file mode 100644 index 0d6788a7e..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration.ts +++ /dev/null @@ -1,12 +0,0 @@ -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, - selectable: false, -} satisfies Partial; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu.ts new file mode 100644 index 000000000..1d73bbea8 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu.ts @@ -0,0 +1,65 @@ +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; +import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; +import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; +import { isDefined } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; + +export const useOpenWorkflowEditFilterInCommandMenu = () => { + const { getIcon } = useIcons(); + + const workflowVisualizerWorkflowId = useRecoilComponentValueV2( + workflowVisualizerWorkflowIdComponentState, + ); + const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu(); + + const setWorkflowSelectedNode = useSetRecoilComponentStateV2( + workflowSelectedNodeComponentState, + ); + const setWorkflowDiagram = useSetRecoilComponentStateV2( + workflowDiagramComponentState, + ); + + const openWorkflowEditFilterInCommandMenu = ({ + stepId, + stepName, + }: { + stepId: string; + stepName: string; + }) => { + if (!isDefined(workflowVisualizerWorkflowId)) { + throw new Error( + 'Workflow ID must be configured for the edge when opening a filter in command menu', + ); + } + + setWorkflowSelectedNode(stepId); + + setWorkflowDiagram((diagram) => { + if (!isDefined(diagram)) { + throw new Error('Workflow diagram must be defined'); + } + + return { + ...diagram, + nodes: diagram.nodes.map((node) => ({ + ...node, + selected: false, + })), + }; + }); + + openWorkflowEditStepInCommandMenu( + workflowVisualizerWorkflowId, + stepName, + getIcon(getActionIcon('FILTER')), + ); + }; + + return { + openWorkflowEditFilterInCommandMenu, + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu.ts new file mode 100644 index 000000000..effb37a08 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu.ts @@ -0,0 +1,78 @@ +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { workflowVisualizerWorkflowRunIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowRunIdComponentState'; +import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; +import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; +import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; +import { isDefined } from 'twenty-shared/utils'; +import { StepStatus } from 'twenty-shared/workflow'; +import { useIcons } from 'twenty-ui/display'; + +export const useOpenWorkflowRunFilterInCommandMenu = () => { + const { getIcon } = useIcons(); + + const workflowVisualizerWorkflowId = useRecoilComponentValueV2( + workflowVisualizerWorkflowIdComponentState, + ); + const workflowVisualizerWorkflowRunId = useRecoilComponentValueV2( + workflowVisualizerWorkflowRunIdComponentState, + ); + + const setWorkflowSelectedNode = useSetRecoilComponentStateV2( + workflowSelectedNodeComponentState, + ); + const setWorkflowDiagram = useSetRecoilComponentStateV2( + workflowDiagramComponentState, + ); + + const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu(); + + const openWorkflowRunFilterInCommandMenu = ({ + stepId, + stepName, + stepExecutionStatus, + }: { + stepId: string; + stepName: string; + stepExecutionStatus: StepStatus; + }) => { + if (!isDefined(workflowVisualizerWorkflowId)) { + throw new Error('Workflow ID is required'); + } + + if (!isDefined(workflowVisualizerWorkflowRunId)) { + throw new Error('Workflow run ID is required'); + } + + setWorkflowSelectedNode(stepId); + + setWorkflowDiagram((diagram) => { + if (!isDefined(diagram)) { + throw new Error('Workflow diagram must be defined'); + } + + return { + ...diagram, + nodes: diagram.nodes.map((node) => ({ + ...node, + selected: false, + })), + }; + }); + + openWorkflowRunViewStepInCommandMenu({ + workflowId: workflowVisualizerWorkflowId, + workflowRunId: workflowVisualizerWorkflowRunId, + title: stepName, + icon: getIcon(getActionIcon('FILTER')), + workflowSelectedNode: stepId, + stepExecutionStatus, + }); + }; + + return { + openWorkflowRunFilterInCommandMenu, + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu.ts new file mode 100644 index 000000000..f19d6f35b --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu.ts @@ -0,0 +1,72 @@ +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState'; +import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; +import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; +import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; +import { isDefined } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; + +export const useOpenWorkflowViewFilterInCommandMenu = () => { + const { getIcon } = useIcons(); + + const workflowVisualizerWorkflowId = useRecoilComponentValueV2( + workflowVisualizerWorkflowIdComponentState, + ); + const workflowVisualizerWorkflowVersionId = useRecoilComponentValueV2( + workflowVisualizerWorkflowVersionIdComponentState, + ); + const { openWorkflowViewStepInCommandMenu } = useWorkflowCommandMenu(); + + const setWorkflowSelectedNode = useSetRecoilComponentStateV2( + workflowSelectedNodeComponentState, + ); + const setWorkflowDiagram = useSetRecoilComponentStateV2( + workflowDiagramComponentState, + ); + + const openWorkflowViewFilterInCommandMenu = ({ + stepId, + stepName, + }: { + stepId: string; + stepName: string; + }) => { + if (!workflowVisualizerWorkflowId) { + throw new Error('Workflow ID is required'); + } + + if (!workflowVisualizerWorkflowVersionId) { + throw new Error('Workflow version ID is required'); + } + + setWorkflowSelectedNode(stepId); + + setWorkflowDiagram((diagram) => { + if (!isDefined(diagram)) { + throw new Error('Workflow diagram must be defined'); + } + + return { + ...diagram, + nodes: diagram.nodes.map((node) => ({ + ...node, + selected: false, + })), + }; + }); + + openWorkflowViewStepInCommandMenu({ + workflowId: workflowVisualizerWorkflowId, + workflowVersionId: workflowVisualizerWorkflowVersionId, + title: stepName, + icon: getIcon(getActionIcon('FILTER')), + }); + }; + + return { + openWorkflowViewFilterInCommandMenu, + }; +}; 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 4127c04cd..5a6ea9dc1 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 @@ -5,6 +5,7 @@ import { } from '@/workflow/types/Workflow'; import { FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter'; import { Edge, Node } from '@xyflow/react'; +import { StepStatus } from 'twenty-shared/workflow'; export type WorkflowDiagramStepNode = Node; export type WorkflowDiagramNode = Node; @@ -69,12 +70,12 @@ export type WorkflowDiagramFilterEdgeData = { filterSettings: FilterSettings; name: string; runStatus?: WorkflowRunStepStatus; - isEdgeEditable: boolean; + edgeExecutionStatus?: StepStatus; }; export type WorkflowDiagramDefaultEdgeData = { edgeType: 'default'; - isEdgeEditable: boolean; + edgeExecutionStatus?: StepStatus; }; export type WorkflowDiagramEdgeData = @@ -86,4 +87,14 @@ export type WorkflowDiagramNodeType = | 'empty-trigger' | 'create-step'; -export type WorkflowDiagramEdgeType = 'default' | 'success'; +export type WorkflowDiagramEdgeType = + | 'blank' + | 'filtering-disabled--editable' + | 'filtering-disabled--readonly' + | 'filtering-disabled--run' + | 'empty-filter--editable' + | 'empty-filter--readonly' + | 'empty-filter--run' + | 'filter--editable' + | 'filter--readonly' + | 'filter--run'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/addCreateStepNodes.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/addCreateStepNodes.test.ts index 082371f3a..11484fc67 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/addCreateStepNodes.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/addCreateStepNodes.test.ts @@ -53,7 +53,11 @@ describe('addCreateStepNodes', () => { }, ]; - const diagramInitial = generateWorkflowDiagram({ trigger, steps }); + const diagramInitial = generateWorkflowDiagram({ + trigger, + steps, + defaultEdgeType: 'empty-filter--editable', + }); expect(diagramInitial.nodes).toHaveLength(3); expect(diagramInitial.edges).toHaveLength(2); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/addEdgeOptions.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/addEdgeOptions.test.ts deleted file mode 100644 index d30e4d670..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/addEdgeOptions.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { addEdgeOptions } from '../addEdgeOptions'; - -describe('addEdgeOptions', () => { - it('should add isEdgeEditable to all edges', () => { - const diagram: WorkflowDiagram = { - nodes: [ - { - id: 'trigger', - position: { x: 0, y: 0 }, - data: { - nodeType: 'trigger', - triggerType: 'DATABASE_EVENT', - name: 'Company Created', - icon: 'IconPlus', - }, - }, - { - id: 'action-1', - position: { x: 0, y: 100 }, - data: { - nodeType: 'action', - actionType: 'CREATE_RECORD', - name: 'Create Company', - }, - }, - ], - edges: [ - { - id: 'edge-1', - source: 'trigger', - target: 'action-1', - data: { - edgeType: 'default', - isEdgeEditable: true, - }, - }, - { - id: 'edge-2', - source: 'action-1', - target: 'action-2', - data: { - edgeType: 'default', - isEdgeEditable: false, - }, - }, - ], - }; - - const result = addEdgeOptions(diagram); - - expect(result.nodes).toEqual(diagram.nodes); - expect(result.edges).toHaveLength(2); - - expect(result.edges[0]).toEqual({ - id: 'edge-1', - source: 'trigger', - target: 'action-1', - data: { - edgeType: 'default', - isEdgeEditable: true, - }, - }); - - expect(result.edges[1]).toEqual({ - id: 'edge-2', - source: 'action-1', - target: 'action-2', - data: { - edgeType: 'default', - isEdgeEditable: true, - }, - }); - }); - - it('should handle empty edges array', () => { - const diagram: WorkflowDiagram = { - nodes: [ - { - id: 'trigger', - position: { x: 0, y: 0 }, - data: { - nodeType: 'trigger', - triggerType: 'DATABASE_EVENT', - name: 'Company Created', - icon: 'IconPlus', - }, - }, - ], - edges: [], - }; - - const result = addEdgeOptions(diagram); - - expect(result.nodes).toEqual(diagram.nodes); - expect(result.edges).toEqual([]); - }); - - it('should handle edges without existing data property', () => { - const diagram: WorkflowDiagram = { - nodes: [ - { - id: 'trigger', - position: { x: 0, y: 0 }, - data: { - nodeType: 'trigger', - triggerType: 'MANUAL', - name: 'Manual Trigger', - icon: 'IconClick', - }, - }, - { - id: 'action-1', - position: { x: 0, y: 100 }, - data: { - nodeType: 'action', - actionType: 'SEND_EMAIL', - name: 'Send Email', - }, - }, - ], - edges: [ - { - id: 'edge-1', - source: 'trigger', - target: 'action-1', - } as any, - ], - }; - - expect(() => addEdgeOptions(diagram)).toThrow('Edge data must be defined'); - }); -}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowDiagram.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowDiagram.test.ts index 35f0602e5..1c127d41e 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowDiagram.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowDiagram.test.ts @@ -13,7 +13,11 @@ describe('generateWorkflowDiagram', () => { }; const steps: WorkflowStep[] = []; - const result = generateWorkflowDiagram({ trigger, steps }); + const result = generateWorkflowDiagram({ + trigger, + steps, + defaultEdgeType: 'empty-filter--editable', + }); expect(result.nodes).toHaveLength(1); expect(result.edges).toHaveLength(0); @@ -75,7 +79,11 @@ describe('generateWorkflowDiagram', () => { }, ]; - const result = generateWorkflowDiagram({ trigger, steps }); + const result = generateWorkflowDiagram({ + trigger, + steps, + defaultEdgeType: 'empty-filter--editable', + }); expect(result.nodes).toHaveLength(steps.length + 1); // All steps + trigger expect(result.edges).toHaveLength(steps.length - 1 + 1); // Edges are one less than nodes + the edge from the trigger to the first node @@ -143,7 +151,11 @@ describe('generateWorkflowDiagram', () => { }, ]; - const result = generateWorkflowDiagram({ trigger, steps }); + const result = generateWorkflowDiagram({ + trigger, + steps, + defaultEdgeType: 'empty-filter--editable', + }); expect(result.edges.length).toEqual(2); expect(result.nodes.length).toEqual(3); @@ -205,7 +217,11 @@ describe('generateWorkflowDiagram', () => { }, ]; - const result = generateWorkflowDiagram({ trigger, steps }); + const result = generateWorkflowDiagram({ + trigger, + steps, + defaultEdgeType: 'empty-filter--editable', + }); expect(result.edges.length).toEqual(2); expect(result.nodes.length).toEqual(3); @@ -286,7 +302,11 @@ describe('generateWorkflowDiagram', () => { }, ]; - const result = generateWorkflowDiagram({ trigger, steps }); + const result = generateWorkflowDiagram({ + trigger, + steps, + defaultEdgeType: 'empty-filter--editable', + }); expect(result.edges.length).toEqual(4); expect(result.nodes.length).toEqual(4); 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 c30093ec1..c8908c3aa 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 @@ -1,8 +1,8 @@ import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow'; import { FieldMetadataType } from 'twenty-shared/types'; +import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow'; import { getUuidV4Mock } from '~/testing/utils/getUuidV4Mock'; import { generateWorkflowRunDiagram } from '../generateWorkflowRunDiagram'; -import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow'; jest.mock('uuid', () => ({ v4: getUuidV4Mock(), @@ -100,6 +100,7 @@ describe('generateWorkflowRunDiagram', () => { trigger, steps, stepInfos, + isWorkflowFilteringEnabled: true, }); expect(result).toMatchInlineSnapshot(` @@ -108,22 +109,22 @@ describe('generateWorkflowRunDiagram', () => { "edges": [ { "data": { + "edgeExecutionStatus": "SUCCESS", "edgeType": "default", - "isEdgeEditable": false, }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-0", - "markerEnd": "workflow-edge-green-arrow-rounded", - "markerStart": "workflow-edge-green-circle", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", "selectable": false, "source": "trigger", "target": "step1", - "type": "success", + "type": "empty-filter--run", }, { "data": { + "edgeExecutionStatus": "FAILED", "edgeType": "default", - "isEdgeEditable": false, }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-1", @@ -132,11 +133,12 @@ describe('generateWorkflowRunDiagram', () => { "selectable": false, "source": "step1", "target": "step2", + "type": "empty-filter--run", }, { "data": { + "edgeExecutionStatus": "NOT_STARTED", "edgeType": "default", - "isEdgeEditable": false, }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-2", @@ -145,6 +147,7 @@ describe('generateWorkflowRunDiagram', () => { "selectable": false, "source": "step2", "target": "step3", + "type": "empty-filter--run", }, ], "nodes": [ @@ -301,6 +304,7 @@ describe('generateWorkflowRunDiagram', () => { trigger, steps, stepInfos, + isWorkflowFilteringEnabled: true, }); expect(result).toMatchInlineSnapshot(` @@ -309,45 +313,45 @@ describe('generateWorkflowRunDiagram', () => { "edges": [ { "data": { + "edgeExecutionStatus": "SUCCESS", "edgeType": "default", - "isEdgeEditable": false, }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-3", - "markerEnd": "workflow-edge-green-arrow-rounded", - "markerStart": "workflow-edge-green-circle", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", "selectable": false, "source": "trigger", "target": "step1", - "type": "success", + "type": "empty-filter--run", }, { "data": { + "edgeExecutionStatus": "SUCCESS", "edgeType": "default", - "isEdgeEditable": false, }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-4", - "markerEnd": "workflow-edge-green-arrow-rounded", - "markerStart": "workflow-edge-green-circle", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", "selectable": false, "source": "step1", "target": "step2", - "type": "success", + "type": "empty-filter--run", }, { "data": { + "edgeExecutionStatus": "SUCCESS", "edgeType": "default", - "isEdgeEditable": false, }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-5", - "markerEnd": "workflow-edge-green-arrow-rounded", - "markerStart": "workflow-edge-green-circle", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", "selectable": false, "source": "step2", "target": "step3", - "type": "success", + "type": "empty-filter--run", }, ], "nodes": [ @@ -504,6 +508,7 @@ describe('generateWorkflowRunDiagram', () => { trigger, steps, stepInfos, + isWorkflowFilteringEnabled: true, }); expect(result).toMatchInlineSnapshot(` @@ -512,22 +517,22 @@ describe('generateWorkflowRunDiagram', () => { "edges": [ { "data": { + "edgeExecutionStatus": "SUCCESS", "edgeType": "default", - "isEdgeEditable": false, }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-6", - "markerEnd": "workflow-edge-green-arrow-rounded", - "markerStart": "workflow-edge-green-circle", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", "selectable": false, "source": "trigger", "target": "step1", - "type": "success", + "type": "empty-filter--run", }, { "data": { + "edgeExecutionStatus": "RUNNING", "edgeType": "default", - "isEdgeEditable": false, }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-7", @@ -536,11 +541,12 @@ describe('generateWorkflowRunDiagram', () => { "selectable": false, "source": "step1", "target": "step2", + "type": "empty-filter--run", }, { "data": { + "edgeExecutionStatus": "NOT_STARTED", "edgeType": "default", - "isEdgeEditable": false, }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-8", @@ -549,6 +555,7 @@ describe('generateWorkflowRunDiagram', () => { "selectable": false, "source": "step2", "target": "step3", + "type": "empty-filter--run", }, ], "nodes": [ @@ -724,6 +731,7 @@ describe('generateWorkflowRunDiagram', () => { trigger, steps, stepInfos, + isWorkflowFilteringEnabled: true, }); expect(result).toMatchInlineSnapshot(` @@ -732,36 +740,36 @@ describe('generateWorkflowRunDiagram', () => { "edges": [ { "data": { + "edgeExecutionStatus": "SUCCESS", "edgeType": "default", - "isEdgeEditable": false, }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-9", - "markerEnd": "workflow-edge-green-arrow-rounded", - "markerStart": "workflow-edge-green-circle", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", "selectable": false, "source": "trigger", "target": "step1", - "type": "success", + "type": "empty-filter--run", }, { "data": { + "edgeExecutionStatus": "SUCCESS", "edgeType": "default", - "isEdgeEditable": false, }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-10", - "markerEnd": "workflow-edge-green-arrow-rounded", - "markerStart": "workflow-edge-green-circle", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", "selectable": false, "source": "step1", "target": "step2", - "type": "success", + "type": "empty-filter--run", }, { "data": { + "edgeExecutionStatus": "RUNNING", "edgeType": "default", - "isEdgeEditable": false, }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-11", @@ -770,11 +778,12 @@ describe('generateWorkflowRunDiagram', () => { "selectable": false, "source": "step2", "target": "step3", + "type": "empty-filter--run", }, { "data": { + "edgeExecutionStatus": "NOT_STARTED", "edgeType": "default", - "isEdgeEditable": false, }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-12", @@ -783,6 +792,7 @@ describe('generateWorkflowRunDiagram', () => { "selectable": false, "source": "step3", "target": "step4", + "type": "empty-filter--run", }, ], "nodes": [ @@ -918,6 +928,7 @@ describe('generateWorkflowRunDiagram', () => { trigger, steps, stepInfos, + isWorkflowFilteringEnabled: true, }); expect(result).toMatchInlineSnapshot(` @@ -926,17 +937,17 @@ describe('generateWorkflowRunDiagram', () => { "edges": [ { "data": { + "edgeExecutionStatus": "SUCCESS", "edgeType": "default", - "isEdgeEditable": false, }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-13", - "markerEnd": "workflow-edge-green-arrow-rounded", - "markerStart": "workflow-edge-green-circle", + "markerEnd": "workflow-edge-arrow-rounded", + "markerStart": "workflow-edge-gray-circle", "selectable": false, "source": "trigger", "target": "step1", - "type": "success", + "type": "empty-filter--run", }, ], "nodes": [ diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowDiagramTriggerNode.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowDiagramTriggerNode.test.ts new file mode 100644 index 000000000..fc7f734f8 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowDiagramTriggerNode.test.ts @@ -0,0 +1,207 @@ +import { WorkflowTrigger } from '@/workflow/types/Workflow'; +import { getWorkflowDiagramTriggerNode } from '../getWorkflowDiagramTriggerNode'; + +describe('getWorkflowDiagramTriggerNode', () => { + describe('MANUAL trigger type', () => { + it('should create trigger node with default label when trigger name is not provided', () => { + const trigger: WorkflowTrigger = { + type: 'MANUAL', + settings: { + objectType: 'person', + outputSchema: {}, + icon: 'IconUser', + }, + }; + + const result = getWorkflowDiagramTriggerNode({ trigger }); + + expect(result).toMatchInlineSnapshot(` + { + "data": { + "icon": "IconHandMove", + "name": "Manual trigger", + "nodeType": "trigger", + "triggerType": "MANUAL", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, + } + `); + }); + }); + + describe('CRON trigger type', () => { + it('should create trigger node for CRON trigger', () => { + const trigger: WorkflowTrigger = { + type: 'CRON', + settings: { + type: 'DAYS', + schedule: { + day: 1, + hour: 9, + minute: 0, + }, + outputSchema: {}, + }, + }; + + const result = getWorkflowDiagramTriggerNode({ trigger }); + + expect(result).toMatchInlineSnapshot(` + { + "data": { + "icon": "IconClock", + "name": "On a schedule", + "nodeType": "trigger", + "triggerType": "CRON", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, + } + `); + }); + }); + + describe('WEBHOOK trigger type', () => { + it('should create trigger node for WEBHOOK trigger', () => { + const trigger: WorkflowTrigger = { + type: 'WEBHOOK', + settings: { + httpMethod: 'POST', + outputSchema: {}, + expectedBody: {}, + authentication: 'API_KEY', + }, + }; + + const result = getWorkflowDiagramTriggerNode({ trigger }); + + expect(result).toMatchInlineSnapshot(` + { + "data": { + "icon": "IconWebhook", + "name": "Webhook", + "nodeType": "trigger", + "triggerType": "WEBHOOK", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, + } + `); + }); + }); + + describe('DATABASE_EVENT trigger type', () => { + it('should create trigger node for DATABASE_EVENT trigger with created event', () => { + const trigger: WorkflowTrigger = { + type: 'DATABASE_EVENT', + settings: { + eventName: 'company.created', + outputSchema: {}, + objectType: 'company', + }, + }; + + const result = getWorkflowDiagramTriggerNode({ trigger }); + + expect(result).toMatchInlineSnapshot(` + { + "data": { + "icon": "IconPlaylistAdd", + "name": "Record is created", + "nodeType": "trigger", + "triggerType": "DATABASE_EVENT", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, + } + `); + }); + + it('should create trigger node with empty label for DATABASE_EVENT trigger with unknown event', () => { + const trigger: WorkflowTrigger = { + type: 'DATABASE_EVENT', + settings: { + eventName: 'company.unknownEvent', + outputSchema: {}, + objectType: 'company', + }, + }; + + const result = getWorkflowDiagramTriggerNode({ trigger }); + + expect(result).toMatchInlineSnapshot(` + { + "data": { + "icon": undefined, + "name": "", + "nodeType": "trigger", + "triggerType": "DATABASE_EVENT", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, + } + `); + }); + }); + + describe('custom trigger name', () => { + it('should use custom name when trigger name is provided', () => { + const trigger: WorkflowTrigger = { + type: 'MANUAL', + name: 'Custom Trigger Name', + settings: { + objectType: 'person', + outputSchema: {}, + icon: 'IconUser', + }, + }; + + const result = getWorkflowDiagramTriggerNode({ trigger }); + + expect(result).toMatchInlineSnapshot(` + { + "data": { + "icon": "IconHandMove", + "name": "Custom Trigger Name", + "nodeType": "trigger", + "triggerType": "MANUAL", + }, + "id": "trigger", + "position": { + "x": 0, + "y": 0, + }, + } + `); + }); + }); + + describe('default case', () => { + it('should throw error for unsupported trigger type', () => { + const trigger = { + type: 'UNSUPPORTED_TYPE', + settings: {}, + } as unknown as WorkflowTrigger; + + expect(() => getWorkflowDiagramTriggerNode({ trigger })).toThrow( + 'Expected the trigger "{"type":"UNSUPPORTED_TYPE","settings":{}}" to be supported.', + ); + }); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowRunStatusTagProps.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowRunStatusTagProps.test.ts new file mode 100644 index 000000000..d9a2e0e35 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowRunStatusTagProps.test.ts @@ -0,0 +1,82 @@ +import { WorkflowRunStatus } from '@/workflow/types/Workflow'; +import { getWorkflowRunStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps'; + +describe('getWorkflowRunStatusTagProps', () => { + it('should return gray color and "Not started" text for NOT_STARTED status', () => { + const result = getWorkflowRunStatusTagProps({ + workflowRunStatus: 'NOT_STARTED', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "color": "gray", + "text": "Not started", +} +`); + }); + + it('should return yellow color and "Running" text for RUNNING status', () => { + const result = getWorkflowRunStatusTagProps({ + workflowRunStatus: 'RUNNING', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "color": "yellow", + "text": "Running", +} +`); + }); + + it('should return green color and "Completed" text for COMPLETED status', () => { + const result = getWorkflowRunStatusTagProps({ + workflowRunStatus: 'COMPLETED', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "color": "green", + "text": "Completed", +} +`); + }); + + it('should return blue color and "Enqueued" text for ENQUEUED status', () => { + const result = getWorkflowRunStatusTagProps({ + workflowRunStatus: 'ENQUEUED', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "color": "blue", + "text": "Enqueued", +} +`); + }); + + it('should return red color and "Failed" text for FAILED status', () => { + const result = getWorkflowRunStatusTagProps({ + workflowRunStatus: 'FAILED', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "color": "red", + "text": "Failed", +} +`); + }); + + it('should return red color and "Failed" text for any unknown status (default case)', () => { + const result = getWorkflowRunStatusTagProps({ + workflowRunStatus: 'UNKNOWN_STATUS' as WorkflowRunStatus, + }); + + expect(result).toMatchInlineSnapshot(` +{ + "color": "red", + "text": "Failed", +} +`); + }); +}); 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 283caae9a..2d104b95e 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 @@ -7,7 +7,11 @@ jest.mock('uuid', () => ({ describe('getWorkflowVersionDiagram', () => { it('returns an empty diagram if the provided workflow version', () => { - const result = getWorkflowVersionDiagram(undefined); + const result = getWorkflowVersionDiagram({ + workflowVersion: undefined, + isEditable: true, + isWorkflowFilteringEnabled: true, + }); expect(result).toMatchInlineSnapshot(` { @@ -19,15 +23,19 @@ describe('getWorkflowVersionDiagram', () => { it('returns a diagram with an empty-trigger node if the provided workflow version has no trigger', () => { const result = getWorkflowVersionDiagram({ - __typename: 'WorkflowVersion', - status: 'ACTIVE', - createdAt: '', - id: '1', - name: '', - steps: [], - trigger: null, - updatedAt: '', - workflowId: '', + workflowVersion: { + __typename: 'WorkflowVersion', + status: 'ACTIVE', + createdAt: '', + id: '1', + name: '', + steps: [], + trigger: null, + updatedAt: '', + workflowId: '', + }, + isEditable: true, + isWorkflowFilteringEnabled: true, }); expect(result).toMatchInlineSnapshot(` @@ -52,19 +60,23 @@ describe('getWorkflowVersionDiagram', () => { it('returns a diagram with only a trigger node if the provided workflow version has no steps', () => { const result = getWorkflowVersionDiagram({ - __typename: 'WorkflowVersion', - status: 'ACTIVE', - createdAt: '', - id: '1', - name: '', - steps: null, - trigger: { - name: 'Record is created', - settings: { eventName: 'company.created', outputSchema: {} }, - type: 'DATABASE_EVENT', + workflowVersion: { + __typename: 'WorkflowVersion', + status: 'ACTIVE', + createdAt: '', + id: '1', + name: '', + steps: null, + trigger: { + name: 'Record is created', + settings: { eventName: 'company.created', outputSchema: {} }, + type: 'DATABASE_EVENT', + }, + updatedAt: '', + workflowId: '', }, - updatedAt: '', - workflowId: '', + isEditable: true, + isWorkflowFilteringEnabled: true, }); expect(result).toMatchInlineSnapshot(` @@ -91,38 +103,42 @@ describe('getWorkflowVersionDiagram', () => { it('returns the diagram for the last version', () => { const result = getWorkflowVersionDiagram({ - __typename: 'WorkflowVersion', - status: 'ACTIVE', - createdAt: '', - id: '1', - name: '', - steps: [ - { - id: 'step-1', - name: '', - settings: { - errorHandlingOptions: { - retryOnFailure: { value: true }, - continueOnFailure: { value: false }, + workflowVersion: { + __typename: 'WorkflowVersion', + status: 'ACTIVE', + createdAt: '', + id: '1', + name: '', + steps: [ + { + id: 'step-1', + name: '', + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', + serverlessFunctionInput: {}, + }, + outputSchema: {}, }, - input: { - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', - serverlessFunctionVersion: '1', - serverlessFunctionInput: {}, - }, - outputSchema: {}, + type: 'CODE', + valid: true, }, - type: 'CODE', - valid: true, + ], + trigger: { + name: 'Company created', + settings: { eventName: 'company.created', outputSchema: {} }, + type: 'DATABASE_EVENT', }, - ], - trigger: { - name: 'Company created', - settings: { eventName: 'company.created', outputSchema: {} }, - type: 'DATABASE_EVENT', + updatedAt: '', + workflowId: '', }, - updatedAt: '', - workflowId: '', + isEditable: true, + isWorkflowFilteringEnabled: true, }); expect(result).toMatchInlineSnapshot(` @@ -131,7 +147,6 @@ describe('getWorkflowVersionDiagram', () => { { "data": { "edgeType": "default", - "isEdgeEditable": false, }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-0", @@ -140,6 +155,7 @@ describe('getWorkflowVersionDiagram', () => { "selectable": false, "source": "trigger", "target": "step-1", + "type": "empty-filter--editable", }, ], "nodes": [ diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionStatusTagProps.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionStatusTagProps.test.ts new file mode 100644 index 000000000..2aed399e9 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionStatusTagProps.test.ts @@ -0,0 +1,69 @@ +import { WorkflowVersionStatus } from '@/workflow/types/Workflow'; +import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps'; + +describe('getWorkflowVersionStatusTagProps', () => { + it('should return gray color and "Archived" text for ARCHIVED status', () => { + const result = getWorkflowVersionStatusTagProps({ + workflowVersionStatus: 'ARCHIVED', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "color": "gray", + "text": "Archived", +} +`); + }); + + it('should return yellow color and "Draft" text for DRAFT status', () => { + const result = getWorkflowVersionStatusTagProps({ + workflowVersionStatus: 'DRAFT', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "color": "yellow", + "text": "Draft", +} +`); + }); + + it('should return green color and "Active" text for ACTIVE status', () => { + const result = getWorkflowVersionStatusTagProps({ + workflowVersionStatus: 'ACTIVE', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "color": "green", + "text": "Active", +} +`); + }); + + it('should return gray color and "Deactivated" text for DEACTIVATED status', () => { + const result = getWorkflowVersionStatusTagProps({ + workflowVersionStatus: 'DEACTIVATED', + }); + + expect(result).toMatchInlineSnapshot(` +{ + "color": "gray", + "text": "Deactivated", +} +`); + }); + + it('should return gray color and "Deactivated" text for any unknown status (default case)', () => { + const result = getWorkflowVersionStatusTagProps({ + workflowVersionStatus: 'UNKNOWN_STATUS' as WorkflowVersionStatus, + }); + + expect(result).toMatchInlineSnapshot(` +{ + "color": "gray", + "text": "Deactivated", +} +`); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts index 8fabd3f33..5164d744d 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts @@ -27,13 +27,16 @@ describe('transformFilterNodesAsEdges', () => { target: 'C', data: { edgeType: 'default', - isEdgeEditable: true, }, }, ], }; - const result = transformFilterNodesAsEdges(diagram); + const result = transformFilterNodesAsEdges({ + nodes: diagram.nodes, + edges: diagram.edges, + defaultFilterEdgeType: 'filter--editable', + }); expect(result.nodes).toEqual(diagram.nodes); expect(result.edges).toEqual(diagram.edges); @@ -67,18 +70,22 @@ describe('transformFilterNodesAsEdges', () => { id: 'A-B', source: 'A', target: 'B', - data: { edgeType: 'default', isEdgeEditable: true }, + data: { edgeType: 'default' }, }, { id: 'B-C', source: 'B', target: 'C', - data: { edgeType: 'default', isEdgeEditable: true }, + data: { edgeType: 'default' }, }, ], }; - const result = transformFilterNodesAsEdges(diagram); + const result = transformFilterNodesAsEdges({ + nodes: diagram.nodes, + edges: diagram.edges, + defaultFilterEdgeType: 'filter--editable', + }); // Should only have nodes A and C expect(result.nodes).toEqual([ @@ -98,6 +105,7 @@ describe('transformFilterNodesAsEdges', () => { expect(result.edges).toHaveLength(1); expect(result.edges[0]).toEqual({ id: 'A-C-filter-B', + type: 'filter--editable', source: 'A', target: 'C', data: { @@ -106,7 +114,6 @@ describe('transformFilterNodesAsEdges', () => { name: 'Filter B', runStatus: undefined, filterSettings: {}, - isEdgeEditable: false, }, }); }); @@ -153,30 +160,34 @@ describe('transformFilterNodesAsEdges', () => { id: 'A-B1', source: 'A', target: 'B1', - data: { edgeType: 'default', isEdgeEditable: true }, + data: { edgeType: 'default' }, }, { id: 'B1-C', source: 'B1', target: 'C', - data: { edgeType: 'default', isEdgeEditable: true }, + data: { edgeType: 'default' }, }, { id: 'C-B2', source: 'C', target: 'B2', - data: { edgeType: 'default', isEdgeEditable: true }, + data: { edgeType: 'default' }, }, { id: 'B2-D', source: 'B2', target: 'D', - data: { edgeType: 'default', isEdgeEditable: true }, + data: { edgeType: 'default' }, }, ], }; - const result = transformFilterNodesAsEdges(diagram); + const result = transformFilterNodesAsEdges({ + nodes: diagram.nodes, + edges: diagram.edges, + defaultFilterEdgeType: 'filter--editable', + }); // Should only have nodes A, C, and D expect(result.nodes).toHaveLength(3); @@ -192,6 +203,7 @@ describe('transformFilterNodesAsEdges', () => { ); expect(edgeAC).toEqual({ id: 'A-C-filter-B1', + type: 'filter--editable', source: 'A', target: 'C', data: { @@ -200,7 +212,6 @@ describe('transformFilterNodesAsEdges', () => { runStatus: undefined, stepId: 'B1', filterSettings: {}, - isEdgeEditable: false, }, }); @@ -209,6 +220,7 @@ describe('transformFilterNodesAsEdges', () => { ); expect(edgeCD).toEqual({ id: 'C-D-filter-B2', + type: 'filter--editable', source: 'C', target: 'D', data: { @@ -217,7 +229,6 @@ describe('transformFilterNodesAsEdges', () => { runStatus: undefined, stepId: 'B2', filterSettings: {}, - isEdgeEditable: false, }, }); }); @@ -241,12 +252,16 @@ describe('transformFilterNodesAsEdges', () => { id: 'A-B', source: 'A', target: 'B', - data: { edgeType: 'default', isEdgeEditable: true }, + data: { edgeType: 'default' }, }, ], }; - const result = transformFilterNodesAsEdges(diagram); + const result = transformFilterNodesAsEdges({ + nodes: diagram.nodes, + edges: diagram.edges, + defaultFilterEdgeType: 'filter--editable', + }); // Should only have node A (filter node B is removed) expect(result.nodes).toEqual([ @@ -293,18 +308,22 @@ describe('transformFilterNodesAsEdges', () => { id: 'trigger-B', source: 'trigger', target: 'B', - data: { edgeType: 'default', isEdgeEditable: true }, + data: { edgeType: 'default' }, }, { id: 'B-C', source: 'B', target: 'C', - data: { edgeType: 'default', isEdgeEditable: true }, + data: { edgeType: 'default' }, }, ], }; - const result = transformFilterNodesAsEdges(diagram); + const result = transformFilterNodesAsEdges({ + nodes: diagram.nodes, + edges: diagram.edges, + defaultFilterEdgeType: 'filter--editable', + }); // Should have trigger and C nodes expect(result.nodes).toEqual([ @@ -328,6 +347,7 @@ describe('transformFilterNodesAsEdges', () => { expect(result.edges).toEqual([ { id: 'trigger-C-filter-B', + type: 'filter--editable', source: 'trigger', target: 'C', data: { @@ -336,7 +356,6 @@ describe('transformFilterNodesAsEdges', () => { runStatus: undefined, stepId: 'B', filterSettings: {}, - isEdgeEditable: false, }, }, ]); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/addCreateStepNodes.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/addCreateStepNodes.ts index c2de38c52..caa4005b3 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/addCreateStepNodes.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/addCreateStepNodes.ts @@ -2,6 +2,7 @@ import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workf import { WorkflowDiagram, WorkflowDiagramEdge, + WorkflowDiagramEdgeType, WorkflowDiagramNode, } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { v4 } from 'uuid'; @@ -31,6 +32,7 @@ export const addCreateStepNodes = ({ nodes, edges }: WorkflowDiagram) => { updatedEdges.push({ ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION, + type: 'blank' as WorkflowDiagramEdgeType, id: v4(), source: node.id, target: newCreateStepNode.id, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/addEdgeOptions.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/addEdgeOptions.ts deleted file mode 100644 index a82d74e4b..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/addEdgeOptions.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { isDefined } from 'twenty-shared/utils'; - -export const addEdgeOptions = ({ - nodes, - edges, -}: WorkflowDiagram): WorkflowDiagram => { - return { - nodes, - edges: edges.map((edge) => { - if (!isDefined(edge.data)) { - throw new Error('Edge data must be defined'); - } - - return { - ...edge, - data: { - ...edge.data, - isEdgeEditable: true, - }, - }; - }), - }; -}; 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 aa9d1374b..ba6d23f48 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 @@ -6,6 +6,7 @@ import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workf import { WorkflowDiagram, WorkflowDiagramEdge, + WorkflowDiagramEdgeType, WorkflowDiagramNode, WorkflowDiagramStepNodeData, } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; @@ -70,12 +71,13 @@ const groupStepsByLevel = (steps: WorkflowStep[]): WorkflowStep[][] => { export const generateWorkflowDiagram = ({ trigger, steps, + defaultEdgeType, }: { trigger: WorkflowTrigger | undefined; steps: Array; + defaultEdgeType: WorkflowDiagramEdgeType; }): WorkflowDiagram => { const nodes: Array = []; - const edges: Array = []; if (isDefined(trigger)) { @@ -112,6 +114,7 @@ export const generateWorkflowDiagram = ({ for (const firstLevelStep of stepsGroupedByLevel[0] || []) { edges.push({ ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION, + type: defaultEdgeType, id: v4(), source: TRIGGER_STEP_ID, target: firstLevelStep.id, @@ -122,6 +125,7 @@ export const generateWorkflowDiagram = ({ step.nextStepIds?.forEach((child) => { edges.push({ ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION, + type: defaultEdgeType, id: v4(), source: step.id, target: child, 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 bcf58ecbc..f2c9f6cdd 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 @@ -1,6 +1,7 @@ import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow'; -import { WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration'; import { + WorkflowDiagramEdgeData, + WorkflowDiagramEdgeType, WorkflowRunDiagram, WorkflowRunDiagramNode, WorkflowRunDiagramStepNodeData, @@ -9,16 +10,18 @@ import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/gener import { isStepNode } from '@/workflow/workflow-diagram/utils/isStepNode'; import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges'; import { isDefined } from 'twenty-shared/utils'; -import { WorkflowRunStepInfos, StepStatus } from 'twenty-shared/workflow'; +import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow'; export const generateWorkflowRunDiagram = ({ trigger, steps, stepInfos, + isWorkflowFilteringEnabled, }: { trigger: WorkflowTrigger; steps: Array; stepInfos: WorkflowRunStepInfos | undefined; + isWorkflowFilteringEnabled: boolean; }): { diagram: WorkflowRunDiagram; stepToOpenByDefault: @@ -35,9 +38,11 @@ export const generateWorkflowRunDiagram = ({ } | undefined = undefined; - const workflowDiagram = transformFilterNodesAsEdges( - generateWorkflowDiagram({ trigger, steps }), - ); + const workflowDiagram = generateWorkflowDiagram({ + trigger, + steps, + defaultEdgeType: 'filtering-disabled--readonly', + }); const workflowRunDiagramNodes: WorkflowRunDiagramNode[] = workflowDiagram.nodes.filter(isStepNode).map((node) => { @@ -76,30 +81,42 @@ export const generateWorkflowRunDiagram = ({ ); if (!isDefined(parentNode)) { - return edge; + throw new Error('Expected the edge to have a parent node'); } const stepInfo = stepInfos?.[parentNode.id]; - if (!isDefined(stepInfo)) { - return edge; - } + const edgeType: WorkflowDiagramEdgeType = isWorkflowFilteringEnabled + ? 'empty-filter--run' + : 'filtering-disabled--run'; - if (stepInfo.status === 'SUCCESS') { - return { - ...edge, - ...WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION, - }; - } - - return edge; + return { + ...edge, + type: edgeType, + data: { + ...edge.data, + edgeType: 'default', + edgeExecutionStatus: stepInfo?.status ?? StepStatus.NOT_STARTED, + } satisfies WorkflowDiagramEdgeData, + }; }); + if (!isWorkflowFilteringEnabled) { + return { + diagram: { + nodes: workflowRunDiagramNodes, + edges: workflowRunDiagramEdges, + }, + stepToOpenByDefault, + }; + } + return { - diagram: { + diagram: transformFilterNodesAsEdges({ nodes: workflowRunDiagramNodes, edges: workflowRunDiagramEdges, - }, + defaultFilterEdgeType: 'filter--run', + }), stepToOpenByDefault, }; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowVersionDiagram.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowVersionDiagram.ts index e50ff47e1..e28c1fb18 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowVersionDiagram.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowVersionDiagram.ts @@ -1,5 +1,8 @@ import { WorkflowVersion } from '@/workflow/types/Workflow'; -import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { + WorkflowDiagram, + WorkflowDiagramEdgeType, +} from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram'; import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges'; import { isDefined } from 'twenty-shared/utils'; @@ -9,17 +12,47 @@ const EMPTY_DIAGRAM: WorkflowDiagram = { edges: [], }; -export const getWorkflowVersionDiagram = ( - workflowVersion: WorkflowVersion | undefined, -): WorkflowDiagram => { +const getEdgeTypeToCreateByDefault = ({ + isWorkflowFilteringEnabled, + isEditable, +}: { + isWorkflowFilteringEnabled: boolean; + isEditable: boolean; +}): WorkflowDiagramEdgeType => { + if (isWorkflowFilteringEnabled) { + return isEditable ? 'empty-filter--editable' : 'empty-filter--readonly'; + } + + return isEditable + ? 'filtering-disabled--editable' + : 'filtering-disabled--readonly'; +}; + +export const getWorkflowVersionDiagram = ({ + workflowVersion, + isWorkflowFilteringEnabled, + isEditable, +}: { + workflowVersion: WorkflowVersion | undefined; + isWorkflowFilteringEnabled: boolean; + isEditable: boolean; +}): WorkflowDiagram => { if (!isDefined(workflowVersion)) { return EMPTY_DIAGRAM; } - return transformFilterNodesAsEdges( - generateWorkflowDiagram({ - trigger: workflowVersion.trigger ?? undefined, - steps: workflowVersion.steps ?? [], + const diagram = generateWorkflowDiagram({ + trigger: workflowVersion.trigger ?? undefined, + steps: workflowVersion.steps ?? [], + defaultEdgeType: getEdgeTypeToCreateByDefault({ + isWorkflowFilteringEnabled, + isEditable, }), - ); + }); + + return transformFilterNodesAsEdges({ + nodes: diagram.nodes, + edges: diagram.edges, + defaultFilterEdgeType: isEditable ? 'filter--editable' : 'filter--readonly', + }); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts index 750a8c351..a5e6bbb99 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts @@ -1,13 +1,22 @@ import { - WorkflowDiagram, WorkflowDiagramEdge, + WorkflowDiagramEdgeType, + WorkflowDiagramNode, } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { isDefined } from 'twenty-shared/utils'; -export const transformFilterNodesAsEdges = ({ +export const transformFilterNodesAsEdges = < + T extends WorkflowDiagramNode, + U extends WorkflowDiagramEdge, +>({ nodes, edges, -}: WorkflowDiagram): WorkflowDiagram => { + defaultFilterEdgeType, +}: { + nodes: T[]; + edges: U[]; + defaultFilterEdgeType: WorkflowDiagramEdgeType; +}): { nodes: T[]; edges: U[] } => { const filterNodes = nodes.filter( (node) => node.data.nodeType === 'action' && @@ -39,18 +48,19 @@ export const transformFilterNodesAsEdges = ({ throw new Error('Expected the filter node to be of action type'); } - const newEdge: WorkflowDiagramEdge = { + const newEdge: U = { ...incomingEdge, + type: defaultFilterEdgeType, id: `${incomingEdge.source}-${outgoingEdge.target}-filter-${filterNode.id}`, target: outgoingEdge.target, data: { + ...incomingEdge.data, edgeType: 'filter', stepId: filterNode.id, // TODO: Get the filter settings from the filter node filterSettings: {}, name: filterNode.data.name, runStatus: filterNode.data.runStatus, - isEdgeEditable: false, }, }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getCronTriggerDefaultSettings.test.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getCronTriggerDefaultSettings.test.ts index a9eff47dd..11904a54f 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getCronTriggerDefaultSettings.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getCronTriggerDefaultSettings.test.ts @@ -1,6 +1,15 @@ import { getCronTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getCronTriggerDefaultSettings'; describe('getCronTriggerDefaultSettings', () => { + it('returns correct settings for DAYS interval', () => { + const result = getCronTriggerDefaultSettings('DAYS'); + expect(result).toEqual({ + schedule: { day: 1, hour: 0, minute: 0 }, + type: 'DAYS', + outputSchema: {}, + }); + }); + it('returns correct settings for HOURS interval', () => { const result = getCronTriggerDefaultSettings('HOURS'); expect(result).toEqual({ diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getManualTriggerDefaultSettings.test.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getManualTriggerDefaultSettings.test.ts index 87d58fa2e..bdd0cbbfd 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getManualTriggerDefaultSettings.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getManualTriggerDefaultSettings.test.ts @@ -1,6 +1,7 @@ +import { WorkflowManualTriggerAvailability } from '@/workflow/types/Workflow'; +import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { getManualTriggerDefaultSettings } from '../getManualTriggerDefaultSettings'; -import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon'; it('returns settings for a manual trigger that can be activated from any where', () => { expect( @@ -28,3 +29,28 @@ it('returns settings for a manual trigger that can be activated from any where', icon: 'IconTest', }); }); + +it('returns settings for WHEN_RECORD_SELECTED with default icon when no custom icon provided', () => { + expect( + getManualTriggerDefaultSettings({ + availability: 'WHEN_RECORD_SELECTED', + activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems, + }), + ).toStrictEqual({ + objectType: generatedMockObjectMetadataItems[0].nameSingular, + outputSchema: {}, + icon: COMMAND_MENU_DEFAULT_ICON, + }); +}); + +it('throws error for unsupported availability type', () => { + const invalidAvailability = + 'INVALID_AVAILABILITY' as WorkflowManualTriggerAvailability; + + expect(() => + getManualTriggerDefaultSettings({ + availability: invalidAvailability, + activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems, + }), + ).toThrow("Didn't expect to get here."); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getTriggerDefaultDefinition.test.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getTriggerDefaultDefinition.test.ts index 41a0fa972..28c700e08 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getTriggerDefaultDefinition.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getTriggerDefaultDefinition.test.ts @@ -100,6 +100,42 @@ describe('getTriggerDefaultDefinition', () => { }); }); + it('returns a valid configuration for CRON trigger type', () => { + expect( + getTriggerDefaultDefinition({ + defaultLabel: 'On a schedule', + type: 'CRON', + activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems, + }), + ).toStrictEqual({ + type: 'CRON', + name: 'On a schedule', + settings: { + type: 'DAYS', + schedule: { day: 1, hour: 0, minute: 0 }, + outputSchema: {}, + }, + }); + }); + + it('returns a valid configuration for WEBHOOK trigger type', () => { + expect( + getTriggerDefaultDefinition({ + defaultLabel: 'Webhook', + type: 'WEBHOOK', + activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems, + }), + ).toStrictEqual({ + type: 'WEBHOOK', + name: 'Webhook', + settings: { + outputSchema: {}, + httpMethod: 'GET', + authentication: null, + }, + }); + }); + it('throws when providing an unknown trigger type', () => { expect(() => { getTriggerDefaultDefinition({ diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getTriggerStepName.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getTriggerStepName.test.ts deleted file mode 100644 index 9e855ae34..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getTriggerStepName.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { getTriggerStepName } from '../getTriggerStepName'; - -it('returns the expected name for a DATABASE_EVENT trigger', () => { - expect( - getTriggerStepName({ - type: 'DATABASE_EVENT', - name: '', - settings: { - eventName: 'company.created', - outputSchema: {}, - }, - }), - ).toBe('Record is created'); -}); - -it('returns the expected name for a MANUAL trigger without a defined objectType', () => { - expect( - getTriggerStepName({ - type: 'MANUAL', - name: '', - settings: { - objectType: undefined, - outputSchema: {}, - }, - }), - ).toBe('Manual trigger'); -}); - -it('returns the expected name for a MANUAL trigger with a defined objectType', () => { - expect( - getTriggerStepName({ - type: 'MANUAL', - name: '', - settings: { - objectType: 'company', - outputSchema: {}, - }, - }), - ).toBe('Manual trigger for Company'); -}); - -it('throws when an unknown trigger type is provided', () => { - expect(() => { - getTriggerStepName({ - type: 'unknown' as any, - name: '', - settings: { - objectType: 'company', - outputSchema: {}, - }, - }); - }).toThrow(); -}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getTriggerStepName.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getTriggerStepName.ts deleted file mode 100644 index 887b9cc82..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getTriggerStepName.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - WorkflowDatabaseEventTrigger, - WorkflowTrigger, -} from '@/workflow/types/Workflow'; -import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; -import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel'; -import { capitalize, isDefined } from 'twenty-shared/utils'; - -export const getTriggerStepName = (trigger: WorkflowTrigger): string => { - switch (trigger.type) { - case 'DATABASE_EVENT': - return getDatabaseEventTriggerStepName(trigger); - case 'CRON': - return 'On a schedule'; - case 'WEBHOOK': - return 'Webhook'; - case 'MANUAL': - if (!isDefined(trigger.settings.objectType)) { - return 'Manual trigger'; - } - - return 'Manual trigger for ' + capitalize(trigger.settings.objectType); - } - - return assertUnreachable(trigger); -}; - -const getDatabaseEventTriggerStepName = ( - trigger: WorkflowDatabaseEventTrigger, -): string => { - const defaultLabel = getTriggerDefaultLabel(trigger); - - return defaultLabel ?? ''; -}; diff --git a/packages/twenty-front/src/utils/__tests__/getWorkspaceUrl.test.ts b/packages/twenty-front/src/utils/__tests__/getWorkspaceUrl.test.ts new file mode 100644 index 000000000..79a60c7f5 --- /dev/null +++ b/packages/twenty-front/src/utils/__tests__/getWorkspaceUrl.test.ts @@ -0,0 +1,70 @@ +import { WorkspaceUrls } from '~/generated/graphql'; +import { getWorkspaceUrl } from '../getWorkspaceUrl'; + +describe('getWorkspaceUrl', () => { + it('should return customUrl when it is defined', () => { + const workspaceUrls: WorkspaceUrls = { + customUrl: 'https://custom.example.com', + subdomainUrl: 'https://subdomain.twenty.com', + }; + + const result = getWorkspaceUrl(workspaceUrls); + + expect(result).toBe('https://custom.example.com'); + }); + + it('should return subdomainUrl when customUrl is null', () => { + const workspaceUrls: WorkspaceUrls = { + customUrl: null, + subdomainUrl: 'https://subdomain.twenty.com', + }; + + const result = getWorkspaceUrl(workspaceUrls); + + expect(result).toBe('https://subdomain.twenty.com'); + }); + + it('should return subdomainUrl when customUrl is undefined', () => { + const workspaceUrls: WorkspaceUrls = { + customUrl: undefined, + subdomainUrl: 'https://subdomain.twenty.com', + }; + + const result = getWorkspaceUrl(workspaceUrls); + + expect(result).toBe('https://subdomain.twenty.com'); + }); + + it('should return customUrl when both customUrl and subdomainUrl are defined', () => { + const workspaceUrls: WorkspaceUrls = { + customUrl: 'https://my-company.com', + subdomainUrl: 'https://mycompany.twenty.com', + }; + + const result = getWorkspaceUrl(workspaceUrls); + + expect(result).toBe('https://my-company.com'); + }); + + it('should return empty string when customUrl is empty string', () => { + const workspaceUrls: WorkspaceUrls = { + customUrl: '', + subdomainUrl: 'https://subdomain.twenty.com', + }; + + const result = getWorkspaceUrl(workspaceUrls); + + expect(result).toBe(''); + }); + + it('should return subdomainUrl when customUrl is explicitly undefined', () => { + const workspaceUrls: WorkspaceUrls = { + customUrl: undefined, + subdomainUrl: 'https://subdomain.twenty.com', + }; + + const result = getWorkspaceUrl(workspaceUrls); + + expect(result).toBe('https://subdomain.twenty.com'); + }); +});