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 index b279c1ec5..dc5be8aba 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge.tsx @@ -1,5 +1,6 @@ import { WorkflowDiagramEdgeV1 } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV1'; -import { WorkflowDiagramEdgeV2 } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2'; +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'; @@ -10,6 +11,7 @@ import { EdgeProps, getStraightPath, } from '@xyflow/react'; +import { isDefined } from 'twenty-shared/utils'; import { FeatureFlagKey } from '~/generated/graphql'; type WorkflowDiagramDefaultEdgeProps = EdgeProps; @@ -36,6 +38,18 @@ export const WorkflowDiagramDefaultEdge = ({ 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 ( <> - {data?.shouldDisplayEdgeOptions && ( - - {isWorkflowFilteringEnabled ? ( - - ) : ( - - )} - - )} + + + {displayEdgeV1 && ( + + )} + {displayEmptyFilters && ( + + )} + {displayFilters && ( + + )} + ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container.tsx new file mode 100644 index 000000000..348b81255 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container.tsx @@ -0,0 +1,13 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; + +const StyledContainer = styled.div<{ labelX: number; labelY: number }>` + padding: ${({ theme }) => theme.spacing(1)}; + pointer-events: all; + ${({ labelX, labelY }) => css` + transform: translate(-50%, -50%) translate(${labelX}px, ${labelY}px); + `} + position: absolute; +`; + +export { StyledContainer as WorkflowDiagramEdgeV2Container }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Empty.tsx similarity index 59% rename from packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2.tsx rename to packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Empty.tsx index be2aa52a8..169dae830 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Empty.tsx @@ -2,29 +2,23 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/ import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined'; -import { WorkflowDiagramEdgeV2Content } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Content'; +import { WorkflowDiagramEdgeV2EmptyContent } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2EmptyContent'; import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep'; -import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep'; -import { isDefined } from 'twenty-shared/utils'; -type WorkflowDiagramEdgeV2Props = { +type WorkflowDiagramEdgeV2EmptyProps = { labelX: number; labelY: number; - stepId: string | undefined; parentStepId: string; nextStepId: string; - filter: Record | undefined; }; -export const WorkflowDiagramEdgeV2 = ({ +export const WorkflowDiagramEdgeV2Empty = ({ labelX, labelY, - stepId, parentStepId, nextStepId, - filter, -}: WorkflowDiagramEdgeV2Props) => { +}: WorkflowDiagramEdgeV2EmptyProps) => { const workflowVisualizerWorkflowId = useRecoilComponentValueV2( workflowVisualizerWorkflowIdComponentState, ); @@ -32,17 +26,14 @@ export const WorkflowDiagramEdgeV2 = ({ assertWorkflowWithCurrentVersionIsDefined(workflow); const { createStep } = useCreateStep({ workflow }); - const { deleteStep } = useDeleteStep({ workflow }); const { startNodeCreation } = useStartNodeCreation(); return ( - { return createStep({ newStepType: 'FILTER', @@ -50,21 +41,8 @@ export const WorkflowDiagramEdgeV2 = ({ nextStepId, }); }} - onDeleteFilter={() => { - if (!isDefined(stepId)) { - throw new Error( - 'Step ID must be configured for the edge when rendering a filter', - ); - } - - return deleteStep(stepId); - }} onCreateNode={() => { - if (isDefined(filter)) { - startNodeCreation({ parentStepId: stepId, nextStepId }); - } else { - startNodeCreation({ parentStepId, nextStepId }); - } + 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 new file mode 100644 index 000000000..402202e26 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2EmptyContent.tsx @@ -0,0 +1,87 @@ +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 new file mode 100644 index 000000000..49dee63fc --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Filter.tsx @@ -0,0 +1,60 @@ +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/WorkflowDiagramEdgeV2Content.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2FilterContent.tsx similarity index 68% rename from packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Content.tsx rename to packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2FilterContent.tsx index 00c7ce9a6..413458985 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Content.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2FilterContent.tsx @@ -9,11 +9,13 @@ import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDrop 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 { css } from '@emotion/react'; +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'; @@ -21,9 +23,7 @@ import { isDefined } from 'twenty-shared/utils'; import { IconDotsVertical, IconFilter, - IconFilterPlus, IconFilterX, - IconGitBranchDeleted, IconPlus, } from 'twenty-ui/display'; import { IconButtonGroup } from 'twenty-ui/input'; @@ -33,49 +33,33 @@ const StyledIconButtonGroup = styled(IconButtonGroup)` pointer-events: all; `; -const StyledRoundedIconButtonGroup = styled(IconButtonGroup)` - border-radius: 50px; - overflow: hidden; - pointer-events: all; +const StyledConfiguredFilterContainer = styled.div` + height: 26px; + width: 26px; `; -const StyledContainer = styled.div<{ labelX: number; labelY: number }>` - padding: ${({ theme }) => theme.spacing(1)}; - pointer-events: all; - ${({ labelX, labelY }) => css` - transform: translate(-50%, -50%) translate(${labelX}px, ${labelY}px); - `} - position: absolute; -`; - -const StyledOpacityOverlay = styled.div<{ shouldDisplay: boolean }>` - opacity: ${({ shouldDisplay }) => (shouldDisplay ? 1 : 0)}; - position: relative; -`; - -type WorkflowDiagramEdgeV2ContentProps = { +type WorkflowDiagramEdgeV2FilterContentProps = { labelX: number; labelY: number; - stepId: string | undefined; + stepId: string; parentStepId: string; nextStepId: string; - filter: Record | undefined; - onCreateFilter: () => Promise; + filterSettings: FilterSettings; onDeleteFilter: () => Promise; onCreateNode: () => void; + isEdgeEditable: boolean; }; -export const WorkflowDiagramEdgeV2Content = ({ +export const WorkflowDiagramEdgeV2FilterContent = ({ labelX, labelY, stepId, parentStepId, nextStepId, - filter, - onCreateFilter, onDeleteFilter, onCreateNode, -}: WorkflowDiagramEdgeV2ContentProps) => { + isEdgeEditable, +}: WorkflowDiagramEdgeV2FilterContentProps) => { const { openDropdown } = useOpenDropdown(); const { closeDropdown } = useCloseDropdown(); @@ -108,71 +92,74 @@ export const WorkflowDiagramEdgeV2Content = ({ const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu(); - const handleCreateFilter = async () => { - await onCreateFilter(); - - closeDropdown(dropdownId); - setHovered(false); - }; - const setWorkflowSelectedNode = useSetRecoilComponentStateV2( workflowSelectedNodeComponentState, ); + const handleMouseEnter = () => { + if (!isEdgeEditable) { + return; + } + + setHovered(true); + }; + + const handleMouseLeave = () => { + setHovered(false); + }; + const handleFilterButtonClick = () => { setWorkflowSelectedNode(stepId); - if (isDefined(filter) && isDefined(workflowVisualizerWorkflowId)) { + + if (isDefined(workflowVisualizerWorkflowId)) { openWorkflowEditStepInCommandMenu( workflowVisualizerWorkflowId, 'Filter', IconFilter, ); - } else { - handleCreateFilter(); } }; return ( - setHovered(true)} - onMouseLeave={() => setHovered(false)} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} > - - {isDefined(filter) && !hovered && !isDropdownOpen && !isSelected ? ( - - ) : ( - { - openDropdown({ - dropdownComponentInstanceIdFromProps: dropdownId, - }); + + + {hovered || isDropdownOpen || isSelected ? ( + - )} + { + Icon: IconDotsVertical, + onClick: () => { + openDropdown({ + dropdownComponentInstanceIdFromProps: dropdownId, + }); + }, + }, + ]} + /> + ) : ( + + )} + { @@ -196,7 +183,12 @@ export const WorkflowDiagramEdgeV2Content = ({ {}} + onClick={() => { + closeDropdown(dropdownId); + setHovered(false); + + handleFilterButtonClick(); + }} /> - {}} - /> } /> - - + + ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer.tsx new file mode 100644 index 000000000..63e6cf1a3 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer.tsx @@ -0,0 +1,8 @@ +import styled from '@emotion/styled'; + +const StyledContainer = styled.div<{ shouldDisplay: boolean }>` + opacity: ${({ shouldDisplay }) => (shouldDisplay ? 1 : 0)}; + position: relative; +`; + +export { StyledContainer as WorkflowDiagramEdgeV2VisibilityContainer }; 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 new file mode 100644 index 000000000..5a12f537d --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2EmptyContent.stories.tsx @@ -0,0 +1,94 @@ +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__/WorkflowDiagramEdgeV2Content.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2FilterContent.stories.tsx similarity index 79% rename from packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2Content.stories.tsx rename to packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2FilterContent.stories.tsx index d52dc3ce4..480ef303f 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2Content.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2FilterContent.stories.tsx @@ -7,11 +7,11 @@ import { getCanvasElementForDropdownTesting, } from 'twenty-ui/testing'; import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator'; -import { WorkflowDiagramEdgeV2Content } from '../WorkflowDiagramEdgeV2Content'; +import { WorkflowDiagramEdgeV2FilterContent } from '../WorkflowDiagramEdgeV2FilterContent'; -const meta: Meta = { - title: 'Modules/Workflow/WorkflowDiagramEdgeV2Content', - component: WorkflowDiagramEdgeV2Content, +const meta: Meta = { + title: 'Modules/Workflow/WorkflowDiagramEdgeV2FilterContent', + component: WorkflowDiagramEdgeV2FilterContent, decorators: [ ComponentDecorator, ReactflowDecorator, @@ -35,14 +35,13 @@ const meta: Meta = { labelY: 0, parentStepId: 'parent-step-id', nextStepId: 'next-step-id', - onCreateFilter: fn(), onDeleteFilter: fn(), onCreateNode: fn(), }, }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const ButtonsAppearOnHover: Story = { play: async ({ canvasElement }) => { @@ -59,27 +58,6 @@ export const ButtonsAppearOnHover: Story = { }, }; -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); 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 6ad2582e4..3b8786598 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 @@ -7,4 +7,8 @@ export const WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION = { 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/types/WorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagram.ts index 335a90f21..4127c04cd 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 @@ -3,18 +3,18 @@ import { WorkflowRunStepStatus, WorkflowTriggerType, } from '@/workflow/types/Workflow'; +import { FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter'; import { Edge, Node } from '@xyflow/react'; export type WorkflowDiagramStepNode = Node; export type WorkflowDiagramNode = Node; -export type WorkflowDiagramEdge = Edge; +export type WorkflowDiagramEdge = Edge; export type WorkflowRunDiagramNode = Node; -export type WorkflowRunDiagramEdge = Edge; export type WorkflowRunDiagram = { nodes: Array; - edges: Array; + edges: Array; }; export type WorkflowDiagram = { @@ -63,12 +63,24 @@ export type WorkflowRunDiagramNodeData = Exclude< 'runStatus' > & { runStatus: WorkflowRunStepStatus }; -export type EdgeData = { - stepId?: string; - filter?: Record; - shouldDisplayEdgeOptions?: boolean; +export type WorkflowDiagramFilterEdgeData = { + edgeType: 'filter'; + stepId: string; + filterSettings: FilterSettings; + name: string; + runStatus?: WorkflowRunStepStatus; + isEdgeEditable: boolean; }; +export type WorkflowDiagramDefaultEdgeData = { + edgeType: 'default'; + isEdgeEditable: boolean; +}; + +export type WorkflowDiagramEdgeData = + | WorkflowDiagramFilterEdgeData + | WorkflowDiagramDefaultEdgeData; + export type WorkflowDiagramNodeType = | 'default' | 'empty-trigger' 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 index efe2dc1bc..d30e4d670 100644 --- 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 @@ -2,7 +2,7 @@ import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagr import { addEdgeOptions } from '../addEdgeOptions'; describe('addEdgeOptions', () => { - it('should add shouldDisplayEdgeOptions to all edges', () => { + it('should add isEdgeEditable to all edges', () => { const diagram: WorkflowDiagram = { nodes: [ { @@ -31,14 +31,18 @@ describe('addEdgeOptions', () => { source: 'trigger', target: 'action-1', data: { - shouldDisplayEdgeOptions: true, + edgeType: 'default', + isEdgeEditable: true, }, }, { id: 'edge-2', source: 'action-1', target: 'action-2', - data: {}, + data: { + edgeType: 'default', + isEdgeEditable: false, + }, }, ], }; @@ -53,7 +57,8 @@ describe('addEdgeOptions', () => { source: 'trigger', target: 'action-1', data: { - shouldDisplayEdgeOptions: true, + edgeType: 'default', + isEdgeEditable: true, }, }); @@ -62,7 +67,8 @@ describe('addEdgeOptions', () => { source: 'action-1', target: 'action-2', data: { - shouldDisplayEdgeOptions: true, + edgeType: 'default', + isEdgeEditable: true, }, }); }); @@ -122,15 +128,6 @@ describe('addEdgeOptions', () => { ], }; - const result = addEdgeOptions(diagram); - - expect(result.edges[0]).toEqual({ - id: 'edge-1', - source: 'trigger', - target: 'action-1', - data: { - shouldDisplayEdgeOptions: true, - }, - }); + expect(() => addEdgeOptions(diagram)).toThrow('Edge data must be defined'); }); }); 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 a73de5577..c30093ec1 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 @@ -107,6 +107,10 @@ describe('generateWorkflowRunDiagram', () => { "diagram": { "edges": [ { + "data": { + "edgeType": "default", + "isEdgeEditable": false, + }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-0", "markerEnd": "workflow-edge-green-arrow-rounded", @@ -117,6 +121,10 @@ describe('generateWorkflowRunDiagram', () => { "type": "success", }, { + "data": { + "edgeType": "default", + "isEdgeEditable": false, + }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-1", "markerEnd": "workflow-edge-arrow-rounded", @@ -126,6 +134,10 @@ describe('generateWorkflowRunDiagram', () => { "target": "step2", }, { + "data": { + "edgeType": "default", + "isEdgeEditable": false, + }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-2", "markerEnd": "workflow-edge-arrow-rounded", @@ -296,6 +308,10 @@ describe('generateWorkflowRunDiagram', () => { "diagram": { "edges": [ { + "data": { + "edgeType": "default", + "isEdgeEditable": false, + }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-3", "markerEnd": "workflow-edge-green-arrow-rounded", @@ -306,6 +322,10 @@ describe('generateWorkflowRunDiagram', () => { "type": "success", }, { + "data": { + "edgeType": "default", + "isEdgeEditable": false, + }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-4", "markerEnd": "workflow-edge-green-arrow-rounded", @@ -316,6 +336,10 @@ describe('generateWorkflowRunDiagram', () => { "type": "success", }, { + "data": { + "edgeType": "default", + "isEdgeEditable": false, + }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-5", "markerEnd": "workflow-edge-green-arrow-rounded", @@ -487,6 +511,10 @@ describe('generateWorkflowRunDiagram', () => { "diagram": { "edges": [ { + "data": { + "edgeType": "default", + "isEdgeEditable": false, + }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-6", "markerEnd": "workflow-edge-green-arrow-rounded", @@ -497,6 +525,10 @@ describe('generateWorkflowRunDiagram', () => { "type": "success", }, { + "data": { + "edgeType": "default", + "isEdgeEditable": false, + }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-7", "markerEnd": "workflow-edge-arrow-rounded", @@ -506,6 +538,10 @@ describe('generateWorkflowRunDiagram', () => { "target": "step2", }, { + "data": { + "edgeType": "default", + "isEdgeEditable": false, + }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-8", "markerEnd": "workflow-edge-arrow-rounded", @@ -695,6 +731,10 @@ describe('generateWorkflowRunDiagram', () => { "diagram": { "edges": [ { + "data": { + "edgeType": "default", + "isEdgeEditable": false, + }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-9", "markerEnd": "workflow-edge-green-arrow-rounded", @@ -705,6 +745,10 @@ describe('generateWorkflowRunDiagram', () => { "type": "success", }, { + "data": { + "edgeType": "default", + "isEdgeEditable": false, + }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-10", "markerEnd": "workflow-edge-green-arrow-rounded", @@ -715,6 +759,10 @@ describe('generateWorkflowRunDiagram', () => { "type": "success", }, { + "data": { + "edgeType": "default", + "isEdgeEditable": false, + }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-11", "markerEnd": "workflow-edge-arrow-rounded", @@ -724,6 +772,10 @@ describe('generateWorkflowRunDiagram', () => { "target": "step3", }, { + "data": { + "edgeType": "default", + "isEdgeEditable": false, + }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-12", "markerEnd": "workflow-edge-arrow-rounded", @@ -873,6 +925,10 @@ describe('generateWorkflowRunDiagram', () => { "diagram": { "edges": [ { + "data": { + "edgeType": "default", + "isEdgeEditable": false, + }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-13", "markerEnd": "workflow-edge-green-arrow-rounded", 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 8fd72cd22..283caae9a 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 @@ -129,6 +129,10 @@ describe('getWorkflowVersionDiagram', () => { { "edges": [ { + "data": { + "edgeType": "default", + "isEdgeEditable": false, + }, "deletable": false, "id": "8f3b2121-f194-4ba4-9fbf-0", "markerEnd": "workflow-edge-arrow-rounded", 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 d28b26a7b..8fabd3f33 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 @@ -25,7 +25,10 @@ describe('transformFilterNodesAsEdges', () => { id: 'A-C', source: 'A', target: 'C', - data: { stepId: 'A', shouldDisplayEdgeOptions: true }, + data: { + edgeType: 'default', + isEdgeEditable: true, + }, }, ], }; @@ -64,13 +67,13 @@ describe('transformFilterNodesAsEdges', () => { id: 'A-B', source: 'A', target: 'B', - data: { stepId: 'A', shouldDisplayEdgeOptions: true }, + data: { edgeType: 'default', isEdgeEditable: true }, }, { id: 'B-C', source: 'B', target: 'C', - data: { stepId: 'B', shouldDisplayEdgeOptions: true }, + data: { edgeType: 'default', isEdgeEditable: true }, }, ], }; @@ -98,9 +101,12 @@ describe('transformFilterNodesAsEdges', () => { source: 'A', target: 'C', data: { - shouldDisplayEdgeOptions: true, + edgeType: 'filter', stepId: 'B', - filter: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B' }, + name: 'Filter B', + runStatus: undefined, + filterSettings: {}, + isEdgeEditable: false, }, }); }); @@ -147,25 +153,25 @@ describe('transformFilterNodesAsEdges', () => { id: 'A-B1', source: 'A', target: 'B1', - data: { stepId: 'A', shouldDisplayEdgeOptions: true }, + data: { edgeType: 'default', isEdgeEditable: true }, }, { id: 'B1-C', source: 'B1', target: 'C', - data: { stepId: 'B1', shouldDisplayEdgeOptions: true }, + data: { edgeType: 'default', isEdgeEditable: true }, }, { id: 'C-B2', source: 'C', target: 'B2', - data: { stepId: 'C', shouldDisplayEdgeOptions: true }, + data: { edgeType: 'default', isEdgeEditable: true }, }, { id: 'B2-D', source: 'B2', target: 'D', - data: { stepId: 'B2', shouldDisplayEdgeOptions: true }, + data: { edgeType: 'default', isEdgeEditable: true }, }, ], }; @@ -189,9 +195,12 @@ describe('transformFilterNodesAsEdges', () => { source: 'A', target: 'C', data: { + edgeType: 'filter', + name: 'Filter B1', + runStatus: undefined, stepId: 'B1', - shouldDisplayEdgeOptions: true, - filter: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B1' }, + filterSettings: {}, + isEdgeEditable: false, }, }); @@ -203,9 +212,12 @@ describe('transformFilterNodesAsEdges', () => { source: 'C', target: 'D', data: { + edgeType: 'filter', + name: 'Filter B2', + runStatus: undefined, stepId: 'B2', - shouldDisplayEdgeOptions: true, - filter: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B2' }, + filterSettings: {}, + isEdgeEditable: false, }, }); }); @@ -229,7 +241,7 @@ describe('transformFilterNodesAsEdges', () => { id: 'A-B', source: 'A', target: 'B', - data: { stepId: 'A', shouldDisplayEdgeOptions: true }, + data: { edgeType: 'default', isEdgeEditable: true }, }, ], }; @@ -281,13 +293,13 @@ describe('transformFilterNodesAsEdges', () => { id: 'trigger-B', source: 'trigger', target: 'B', - data: { stepId: 'trigger', shouldDisplayEdgeOptions: true }, + data: { edgeType: 'default', isEdgeEditable: true }, }, { id: 'B-C', source: 'B', target: 'C', - data: { stepId: 'B', shouldDisplayEdgeOptions: true }, + data: { edgeType: 'default', isEdgeEditable: true }, }, ], }; @@ -319,13 +331,12 @@ describe('transformFilterNodesAsEdges', () => { source: 'trigger', target: 'C', data: { + edgeType: 'filter', + name: 'Filter B', + runStatus: undefined, stepId: 'B', - shouldDisplayEdgeOptions: true, - filter: { - nodeType: 'action', - actionType: 'FILTER', - name: 'Filter B', - }, + filterSettings: {}, + isEdgeEditable: false, }, }, ]); 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 index f37e00111..a82d74e4b 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/addEdgeOptions.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/addEdgeOptions.ts @@ -1,4 +1,5 @@ import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { isDefined } from 'twenty-shared/utils'; export const addEdgeOptions = ({ nodes, @@ -7,11 +8,15 @@ export const addEdgeOptions = ({ return { nodes, edges: edges.map((edge) => { + if (!isDefined(edge.data)) { + throw new Error('Edge data must be defined'); + } + return { ...edge, data: { ...edge.data, - shouldDisplayEdgeOptions: true, + isEdgeEditable: true, }, }; }), 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 67584b881..750a8c351 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,4 +1,7 @@ -import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { + WorkflowDiagram, + WorkflowDiagramEdge, +} from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { isDefined } from 'twenty-shared/utils'; export const transformFilterNodesAsEdges = ({ @@ -29,14 +32,25 @@ export const transformFilterNodesAsEdges = ({ const outgoingEdge = edges.find((edge) => edge.source === filterNode.id); if (isDefined(incomingEdge) && isDefined(outgoingEdge)) { - const newEdge = { + if ( + filterNode.data.nodeType !== 'action' || + filterNode.data.actionType !== 'FILTER' + ) { + throw new Error('Expected the filter node to be of action type'); + } + + const newEdge: WorkflowDiagramEdge = { ...incomingEdge, id: `${incomingEdge.source}-${outgoingEdge.target}-filter-${filterNode.id}`, target: outgoingEdge.target, data: { - ...incomingEdge.data, + edgeType: 'filter', stepId: filterNode.id, - filter: filterNode.data, + // 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-steps/hooks/useDeleteStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteStep.ts index 1431ef1a0..8d19dac73 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteStep.ts @@ -8,12 +8,13 @@ import { WorkflowVersion, WorkflowWithCurrentVersion, } from '@/workflow/types/Workflow'; +import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined'; import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId'; export const useDeleteStep = ({ workflow, }: { - workflow: WorkflowWithCurrentVersion; + workflow: WorkflowWithCurrentVersion | undefined; }) => { const { deleteWorkflowVersionStep } = useDeleteWorkflowVersionStep(); const { updateOneRecord: updateOneWorkflowVersion } = @@ -26,8 +27,12 @@ export const useDeleteStep = ({ const { closeCommandMenu } = useCommandMenu(); const deleteStep = async (stepId: string) => { + assertWorkflowWithCurrentVersionIsDefined(workflow); + closeCommandMenu(); + const workflowVersionId = await getUpdatableWorkflowVersion(workflow); + if (stepId === TRIGGER_STEP_ID) { await updateOneWorkflowVersion({ idToUpdate: workflowVersionId, diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getTriggerHeaderType.test.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getTriggerHeaderType.test.ts new file mode 100644 index 000000000..a13ac085d --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getTriggerHeaderType.test.ts @@ -0,0 +1,251 @@ +import { WorkflowTrigger } from '@/workflow/types/Workflow'; +import { getTriggerHeaderType } from '../getTriggerHeaderType'; + +describe('getTriggerHeaderType', () => { + describe('DATABASE_EVENT triggers', () => { + it('returns "Trigger · Record is created" for created event', () => { + const trigger: WorkflowTrigger = { + type: 'DATABASE_EVENT', + name: 'Company Created', + settings: { + eventName: 'company.created', + outputSchema: {}, + }, + }; + + const result = getTriggerHeaderType(trigger); + + expect(result).toBe('Trigger · Record is created'); + }); + + it('returns "Trigger · Record is updated" for updated event', () => { + const trigger: WorkflowTrigger = { + type: 'DATABASE_EVENT', + name: 'Company Updated', + settings: { + eventName: 'company.updated', + outputSchema: {}, + }, + }; + + const result = getTriggerHeaderType(trigger); + + expect(result).toBe('Trigger · Record is updated'); + }); + + it('returns "Trigger · Record is deleted" for deleted event', () => { + const trigger: WorkflowTrigger = { + type: 'DATABASE_EVENT', + name: 'Company Deleted', + settings: { + eventName: 'company.deleted', + outputSchema: {}, + }, + }; + + const result = getTriggerHeaderType(trigger); + + expect(result).toBe('Trigger · Record is deleted'); + }); + + it('works with different object types', () => { + const trigger: WorkflowTrigger = { + type: 'DATABASE_EVENT', + name: 'Person Created', + settings: { + eventName: 'person.created', + outputSchema: {}, + }, + }; + + const result = getTriggerHeaderType(trigger); + + expect(result).toBe('Trigger · Record is created'); + }); + }); + + describe('MANUAL triggers', () => { + it('returns "Trigger · Manual" for manual trigger', () => { + const trigger: WorkflowTrigger = { + type: 'MANUAL', + name: 'Manual Trigger', + settings: { + objectType: 'company', + outputSchema: {}, + icon: 'IconHandMove', + }, + }; + + const result = getTriggerHeaderType(trigger); + + expect(result).toBe('Trigger · Manual'); + }); + + it('returns "Trigger · Manual" for manual trigger without objectType', () => { + const trigger: WorkflowTrigger = { + type: 'MANUAL', + name: 'Manual Trigger', + settings: { + outputSchema: {}, + icon: 'IconHandMove', + }, + }; + + const result = getTriggerHeaderType(trigger); + + expect(result).toBe('Trigger · Manual'); + }); + }); + + describe('CRON triggers', () => { + it('returns "Trigger" for cron trigger with DAYS schedule', () => { + const trigger: WorkflowTrigger = { + type: 'CRON', + name: 'Scheduled Trigger', + settings: { + type: 'DAYS', + schedule: { + day: 1, + hour: 9, + minute: 0, + }, + outputSchema: {}, + }, + }; + + const result = getTriggerHeaderType(trigger); + + expect(result).toBe('Trigger'); + }); + + it('returns "Trigger" for cron trigger with HOURS schedule', () => { + const trigger: WorkflowTrigger = { + type: 'CRON', + name: 'Hourly Trigger', + settings: { + type: 'HOURS', + schedule: { + hour: 2, + minute: 30, + }, + outputSchema: {}, + }, + }; + + const result = getTriggerHeaderType(trigger); + + expect(result).toBe('Trigger'); + }); + + it('returns "Trigger" for cron trigger with MINUTES schedule', () => { + const trigger: WorkflowTrigger = { + type: 'CRON', + name: 'Minutely Trigger', + settings: { + type: 'MINUTES', + schedule: { + minute: 15, + }, + outputSchema: {}, + }, + }; + + const result = getTriggerHeaderType(trigger); + + expect(result).toBe('Trigger'); + }); + + it('returns "Trigger" for cron trigger with CUSTOM schedule', () => { + const trigger: WorkflowTrigger = { + type: 'CRON', + name: 'Custom Trigger', + settings: { + type: 'CUSTOM', + pattern: '0 9 * * 1', + outputSchema: {}, + }, + }; + + const result = getTriggerHeaderType(trigger); + + expect(result).toBe('Trigger'); + }); + }); + + describe('WEBHOOK triggers', () => { + it('returns "Trigger · Webhook" for webhook trigger with GET method', () => { + const trigger: WorkflowTrigger = { + type: 'WEBHOOK', + name: 'Webhook Trigger', + settings: { + httpMethod: 'GET', + authentication: null, + outputSchema: {}, + }, + }; + + const result = getTriggerHeaderType(trigger); + + expect(result).toBe('Trigger · Webhook'); + }); + + it('returns "Trigger · Webhook" for webhook trigger with POST method', () => { + const trigger: WorkflowTrigger = { + type: 'WEBHOOK', + name: 'Webhook Trigger', + settings: { + httpMethod: 'POST', + expectedBody: { + message: 'Workflow was started', + }, + authentication: null, + outputSchema: { + message: { + icon: 'IconVariable', + isLeaf: true, + label: 'message', + type: 'string', + value: 'Workflow was started', + }, + }, + }, + }; + + const result = getTriggerHeaderType(trigger); + + expect(result).toBe('Trigger · Webhook'); + }); + + it('returns "Trigger · Webhook" for webhook trigger with API_KEY authentication', () => { + const trigger: WorkflowTrigger = { + type: 'WEBHOOK', + name: 'Secure Webhook Trigger', + settings: { + httpMethod: 'GET', + authentication: 'API_KEY', + outputSchema: {}, + }, + }; + + const result = getTriggerHeaderType(trigger); + + expect(result).toBe('Trigger · Webhook'); + }); + }); + + describe('error cases', () => { + it('throws error for unknown trigger type', () => { + const trigger = { + type: 'UNKNOWN_TYPE', + name: 'Unknown Trigger', + settings: { + outputSchema: {}, + }, + } as unknown as WorkflowTrigger; + + expect(() => getTriggerHeaderType(trigger)).toThrow( + 'Unknown trigger type', + ); + }); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getTriggerIconColor.test.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getTriggerIconColor.test.ts new file mode 100644 index 000000000..0d47d2ae8 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getTriggerIconColor.test.ts @@ -0,0 +1,56 @@ +/* eslint-disable @nx/workspace-no-hardcoded-colors */ +import { Theme } from '@emotion/react'; +import { getTriggerIconColor } from '../getTriggerIconColor'; + +describe('getTriggerIconColor', () => { + const mockTheme: Theme = { + font: { + color: { + primary: '#2c2c2c', + secondary: '#666666', + tertiary: '#999999', + light: '#cccccc', + }, + }, + } as unknown as Theme; + + it('returns the tertiary font color from theme', () => { + const result = getTriggerIconColor({ theme: mockTheme }); + + expect(result).toBe('#999999'); + }); + + it('works with different theme configurations', () => { + const differentTheme: Theme = { + font: { + color: { + primary: '#000000', + secondary: '#444444', + tertiary: '#888888', + light: '#ffffff', + }, + }, + } as unknown as Theme; + + const result = getTriggerIconColor({ theme: differentTheme }); + + expect(result).toBe('#888888'); + }); + + it('maintains reference to theme.font.color.tertiary', () => { + const customTheme: Theme = { + font: { + color: { + primary: '#111111', + secondary: '#333333', + tertiary: '#custom-tertiary-color', + light: '#eeeeee', + }, + }, + } as unknown as Theme; + + const result = getTriggerIconColor({ theme: customTheme }); + + expect(result).toBe('#custom-tertiary-color'); + }); +});