diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBaseHandle.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBaseHandle.tsx deleted file mode 100644 index 262ab36e0d..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBaseHandle.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { NODE_HANDLE_HEIGHT_PX } from '@/workflow/workflow-diagram/constants/NodeHandleHeightPx'; -import { NODE_HANDLE_WIDTH_PX } from '@/workflow/workflow-diagram/constants/NodeHandleWidthPx'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { css } from '@emotion/react'; -import styled from '@emotion/styled'; -import { Handle, type HandleProps } from '@xyflow/react'; -import { FeatureFlagKey } from '~/generated/graphql'; - -type WorkflowDiagramBaseHandleProps = HandleProps & { - isVisible?: boolean; - disableHoverEffect?: boolean; - selected?: boolean; -}; - -const HANDLE_SCALE_ON_HOVER = 1.5; - -const TRANSLATE_PERCENT = 33.33; - -const StyledHandle = styled(Handle, { - shouldForwardProp: (prop) => - !['disableHoverEffect', 'selected'].includes(prop), -})` - // We need !important to avoid passing style with the Handle.style property - height: ${NODE_HANDLE_HEIGHT_PX}px !important; - width: ${NODE_HANDLE_WIDTH_PX}px !important; - border-color: ${({ theme, selected }) => - selected ? theme.color.blue : theme.border.color.strong} !important; - background: ${({ theme, selected }) => - selected - ? theme.adaptiveColors.blue1 - : theme.background.primary} !important; - transition: - transform 0.1s ease-out, - background 0.1s, - border-color 0.1s !important; - z-index: 1 !important; - - ${({ position }) => { - switch (position) { - case 'top': - return `top: 19px !important;`; - case 'bottom': - return `top: 47px !important;`; - default: - return ''; - } - }} - - ${({ disableHoverEffect, theme, position }) => { - if (disableHoverEffect === true) { - return undefined; - } - - let transform = css` - transform: scale(${HANDLE_SCALE_ON_HOVER}) !important; - `; - - if (position === 'top') { - transform = css` - transform: scale(${HANDLE_SCALE_ON_HOVER}) - translate(${-TRANSLATE_PERCENT}%, ${-TRANSLATE_PERCENT}%) !important; - `; - } else if (position === 'bottom') { - transform = css` - transform: scale(${HANDLE_SCALE_ON_HOVER}) - translate(${-TRANSLATE_PERCENT}%, ${TRANSLATE_PERCENT * 2}%) !important; - `; - } - - return css` - &:hover { - background: ${theme.adaptiveColors.blue1} !important; - border-color: ${theme.color.blue} !important; - ${transform} - } - `; - }} -`; - -export const WorkflowDiagramBaseHandle = ({ - type, - position, - isVisible = true, - selected = false, -}: WorkflowDiagramBaseHandleProps) => { - const isWorkflowBranchEnabled = useIsFeatureEnabled( - FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED, - ); - - return ( - - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx index 2807892aa0..9178a070a7 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx @@ -72,20 +72,6 @@ const StyledResetReactflowStyles = styled.div` white-space: nowrap; } - .react-flow__handle { - min-height: 0; - min-width: 0; - } - .react-flow__handle-top { - transform: translate(-50%, -50%); - } - .react-flow__handle-bottom { - transform: translate(-50%, 100%); - } - .react-flow__handle.connectionindicator { - cursor: pointer; - } - --xy-node-border-radius: none; --xy-node-border: none; --xy-node-background-color: none; 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 a7651b61c2..f77bf76694 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 @@ -6,10 +6,7 @@ import { WorkflowDiagramBlankEdge } from '@/workflow/workflow-diagram/components import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase'; import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect'; import { WorkflowDiagramDefaultEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable'; -import { WorkflowDiagramEmptyTriggerEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerEditable'; -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 { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; import { workflowDiagramRightClickMenuPositionState } from '@/workflow/workflow-diagram/states/workflowDiagramRightClickMenuPositionState'; import { @@ -17,6 +14,9 @@ import { type WorkflowDiagramNode, } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps'; +import { WorkflowDiagramEmptyTriggerEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerEditable'; +import { WorkflowDiagramFilterEdgeEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramFilterEdgeEditable'; +import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditable'; import { useCreateEdge } from '@/workflow/workflow-steps/hooks/useCreateEdge'; import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge'; import { useUpdateStep } from '@/workflow/workflow-steps/hooks/useUpdateStep'; 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 a3ae3b2d4c..867da0e415 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,11 +1,11 @@ import { type WorkflowVersionStatus } from '@/workflow/types/Workflow'; import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase'; import { WorkflowDiagramDefaultEdgeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeReadonly'; -import { WorkflowDiagramEmptyTriggerReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerReadonly'; -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 { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps'; +import { WorkflowDiagramEmptyTriggerReadonly } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerReadonly'; +import { WorkflowDiagramFilterEdgeReadonly } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramFilterEdgeReadonly'; +import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeReadonly'; import { ReactFlowProvider } from '@xyflow/react'; export const WorkflowDiagramCanvasReadonly = ({ diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCustomMarkers.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCustomMarkers.tsx index fd85b03806..2bd3344add 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCustomMarkers.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCustomMarkers.tsx @@ -1,8 +1,4 @@ import { EDGE_BRANCH_ARROW_MARKER_ID } from '@/workflow/workflow-diagram/constants/EdgeBranchArrowMarkerId'; -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_GREEN_ROUNDED_ARROW_MARKER_WIDTH_PX } from '@/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerWidthPx'; -import { EDGE_ROUNDED_ARROW_MARKER_ID } from '@/workflow/workflow-diagram/constants/EdgeRoundedArrowMarkerId'; import { useTheme } from '@emotion/react'; export const WorkflowDiagramCustomMarkers = () => { @@ -11,32 +7,6 @@ export const WorkflowDiagramCustomMarkers = () => { return ( - - - - - - - - { fill={theme.background.primary} /> - - - - ); 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 index 2131f477fc..35c7db08db 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeRun.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeRun.tsx @@ -9,7 +9,8 @@ export const WorkflowDiagramDefaultEdgeRun = ({ sourceY, targetX, targetY, - data, + markerStart, + markerEnd, }: WorkflowDiagramDefaultEdgeRunProps) => { const [edgePath] = getBezierPath({ sourceX, @@ -20,8 +21,9 @@ export const WorkflowDiagramDefaultEdgeRun = ({ return ( ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerEditable.tsx deleted file mode 100644 index b3f2fd5da7..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerEditable.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; -import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; -import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase'; -import styled from '@emotion/styled'; -import { useLingui } from '@lingui/react/macro'; -import { useContext } from 'react'; -import { useSetRecoilState } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; - -const StyledStepNodeLabelIconContainer = styled.div` - align-items: center; - background: ${({ theme }) => theme.background.transparent.light}; - border-radius: ${({ theme }) => theme.spacing(1)}; - display: flex; - justify-content: center; - padding: ${({ theme }) => theme.spacing(3)}; -`; - -export const WorkflowDiagramEmptyTriggerEditable = () => { - const { t } = useLingui(); - - const { openWorkflowTriggerTypeInCommandMenu } = useWorkflowCommandMenu(); - - const workflowVisualizerWorkflowId = useRecoilComponentValue( - workflowVisualizerWorkflowIdComponentState, - ); - - const { isInRightDrawer } = useContext(ActionMenuContext); - - const setCommandMenuNavigationStack = useSetRecoilState( - commandMenuNavigationStackState, - ); - - return ( - } - onClick={() => { - if (!isInRightDrawer) { - setCommandMenuNavigationStack([]); - } - - if (!isDefined(workflowVisualizerWorkflowId)) { - return; - } - - openWorkflowTriggerTypeInCommandMenu(workflowVisualizerWorkflowId); - }} - /> - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerReadonly.tsx deleted file mode 100644 index e384ae9dc5..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerReadonly.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; -import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; -import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; -import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState'; -import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase'; -import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import styled from '@emotion/styled'; -import { useLingui } from '@lingui/react/macro'; -import { useContext } from 'react'; -import { useSetRecoilState } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; -import { TRIGGER_STEP_ID } from 'twenty-shared/workflow'; -import { useIcons } from 'twenty-ui/display'; - -const StyledStepNodeLabelIconContainer = styled.div` - align-items: center; - background: ${({ theme }) => theme.background.transparent.light}; - border-radius: ${({ theme }) => theme.spacing(1)}; - display: flex; - justify-content: center; - padding: ${({ theme }) => theme.spacing(3)}; -`; - -export const WorkflowDiagramEmptyTriggerReadonly = () => { - const { getIcon } = useIcons(); - const { t } = useLingui(); - - const workflowVisualizerWorkflowId = useRecoilComponentValue( - workflowVisualizerWorkflowIdComponentState, - ); - const workflowVisualizerWorkflowVersionId = useRecoilComponentValue( - workflowVisualizerWorkflowVersionIdComponentState, - ); - - const { isInRightDrawer } = useContext(ActionMenuContext); - - const { openWorkflowViewStepInCommandMenu } = useWorkflowCommandMenu(); - - const setWorkflowSelectedNode = useSetRecoilComponentState( - workflowSelectedNodeComponentState, - ); - - const setCommandMenuNavigationStack = useSetRecoilState( - commandMenuNavigationStackState, - ); - - return ( - } - onClick={() => { - if ( - !isDefined(workflowVisualizerWorkflowId) || - !isDefined(workflowVisualizerWorkflowVersionId) - ) { - throw new Error( - 'Workflow ID and Version ID must be defined to open the command menu.', - ); - } - - if (!isInRightDrawer) { - setCommandMenuNavigationStack([]); - } - - setWorkflowSelectedNode(TRIGGER_STEP_ID); - - openWorkflowViewStepInCommandMenu({ - workflowId: workflowVisualizerWorkflowId, - workflowVersionId: workflowVisualizerWorkflowVersionId, - title: t`Add a Trigger`, - icon: getIcon(null), - }); - }} - /> - ); -}; 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 index 3f23f90dd0..55ed180d5c 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeRun.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeRun.tsx @@ -63,6 +63,8 @@ export const WorkflowDiagramFilterEdgeRun = ({ targetX, targetY, data, + markerStart, + markerEnd, }: WorkflowDiagramFilterEdgeRunProps) => { assertFilterEdgeDataOrThrow(data); @@ -98,8 +100,9 @@ export const WorkflowDiagramFilterEdgeRun = ({ 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 index f56c5b3c4a..03ee52df78 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeRun.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeRun.tsx @@ -10,7 +10,8 @@ export const WorkflowDiagramFilteringDisabledEdgeRun = ({ sourceY, targetX, targetY, - data, + markerStart, + markerEnd, }: WorkflowDiagramFilteringDisabledEdgeRunProps) => { const [edgePath] = getBezierPath({ sourceX, @@ -21,8 +22,9 @@ export const WorkflowDiagramFilteringDisabledEdgeRun = ({ return ( ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx deleted file mode 100644 index 92b4761d84..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import { NODE_BORDER_WIDTH } from '@/workflow/workflow-diagram/constants/NodeBorderWidth'; -import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId'; -import { type WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { type WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant'; -import { getWorkflowDiagramNodeSelectedColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramNodeSelectedColors'; -import { css } from '@emotion/react'; -import styled from '@emotion/styled'; -import React, { type MouseEvent } from 'react'; -import { capitalize } from 'twenty-shared/utils'; -import { Label, OverflowingTextWithTooltip } from 'twenty-ui/display'; -import { Loader } from 'twenty-ui/feedback'; -import { WorkflowDiagramBaseHandle } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseHandle'; -import { Position } from '@xyflow/react'; -import { useEdgeSelected } from '@/workflow/workflow-diagram/hooks/useEdgeSelected'; - -const StyledStepNodeContainer = styled.div` - display: flex; - flex-direction: column; -`; - -const StyledStepNodeType = styled.div<{ - nodeVariant: WorkflowDiagramNodeVariant; -}>` - ${({ nodeVariant, theme }) => { - switch (nodeVariant) { - case 'running': { - return css` - background-color: ${theme.tag.background.yellow}; - color: ${theme.tag.text.yellow}; - `; - } - case 'success': { - return css` - background-color: ${theme.tag.background.turquoise}; - color: ${theme.tag.text.turquoise}; - `; - } - case 'failure': { - return css` - background-color: ${theme.tag.background.red}; - color: ${theme.color.red}; - `; - } - default: { - return css` - background-color: ${theme.background.tertiary}; - `; - } - } - }} - - align-self: flex-start; - border-radius: ${({ theme }) => - `${theme.border.radius.sm} ${theme.border.radius.sm} 0 0`}; - margin-left: ${({ theme }) => theme.spacing(2)}; - padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)}; - - .selectable:is(.selected, :focus, :focus-visible) & { - ${({ nodeVariant, theme }) => { - switch (nodeVariant) { - case 'empty': - case 'default': - case 'not-executed': - return css` - background-color: ${theme.color.blue}; - color: ${theme.font.color.inverted}; - `; - } - }} - } -`.withComponent(Label); - -const StyledStepNodeInnerContainer = styled.div<{ - variant: WorkflowDiagramNodeVariant; -}>` - background: ${({ theme }) => theme.background.secondary}; - border-color: ${({ theme }) => theme.border.color.medium}; - - border-radius: ${({ theme }) => theme.border.radius.md}; - border-style: solid; - border-width: ${NODE_BORDER_WIDTH}px; - box-shadow: ${({ variant, theme }) => - variant === 'empty' ? 'none' : theme.boxShadow.strong}; - display: flex; - gap: ${({ theme }) => theme.spacing(2)}; - padding: ${({ theme }) => theme.spacing(2)}; - - position: relative; - - transition: background ${({ theme }) => theme.animation.duration.fast} ease; - - .workflow-node-container:hover & { - ${({ theme }) => { - return css` - background: linear-gradient( - 0deg, - ${theme.background.transparent.lighter} 0%, - ${theme.background.transparent.lighter} 100% - ), - ${theme.background.secondary}; - `; - }} - } - - .selectable:is(.selected, :focus, :focus-visible) - :is(.workflow-node-container, .workflow-node-container:hover) - & { - ${({ theme, variant }) => { - const colors = getWorkflowDiagramNodeSelectedColors(variant, theme); - return css` - background: ${colors.background}; - border-color: ${colors.borderColor}; - `; - }} - } -`; - -const StyledStepNodeLabel = styled.div<{ - variant: WorkflowDiagramNodeVariant; -}>` - box-sizing: border-box; - align-items: center; - display: flex; - font-size: 13px; - font-weight: ${({ theme }) => theme.font.weight.medium}; - column-gap: ${({ theme }) => theme.spacing(2)}; - color: ${({ variant, theme }) => { - switch (variant) { - case 'empty': - case 'not-executed': - return theme.font.color.light; - default: - return theme.font.color.primary; - } - }}; - max-width: 200px; - height: 24px; - - .selectable:is(.selected, :focus, :focus-visible) & { - color: ${({ theme }) => theme.font.color.primary}; - } -`; - -export const WorkflowDiagramStepNodeBase = ({ - id, - nodeType, - name, - variant, - selected, - Icon, - RightFloatingElement, - BottomHoverFloatingElement, - displayHandle = true, - onClick, - onMouseEnter, - onMouseLeave, -}: { - id: string; - nodeType: WorkflowDiagramStepNodeData['nodeType']; - name: string; - variant: WorkflowDiagramNodeVariant; - selected?: boolean; - Icon?: React.ReactNode; - RightFloatingElement?: React.ReactNode; - BottomHoverFloatingElement?: React.ReactNode; - displayHandle?: boolean; - onClick?: () => void; - onMouseEnter?: (event: MouseEvent) => void; - onMouseLeave?: (event: MouseEvent) => void; -}) => { - const { getNodeHandlesSelectedState } = useEdgeSelected(); - - const handlesSelectedState = getNodeHandlesSelectedState(id); - - return ( - - {nodeType !== 'trigger' && ( - - )} - - - {capitalize(nodeType)} - - - - - {variant === 'running' ? : Icon} - - - - - {RightFloatingElement} - - {BottomHoverFloatingElement} - - - - - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent.tsx deleted file mode 100644 index 196cec59d6..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { WorkflowDiagramCreateStepElement } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement'; -import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase'; -import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon'; -import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; -import { type WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { type WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant'; -import styled from '@emotion/styled'; -import { useState } from 'react'; -import { IconTrash } from 'twenty-ui/display'; -import { FloatingIconButton } from 'twenty-ui/input'; - -const StyledDeleteButtonContainer = styled.div` - display: flex; - align-items: center; - position: absolute; - right: ${({ theme }) => theme.spacing(-4)}; - bottom: 0; - top: 0; - transform: translateX(100%); -`; - -const StyledAddStepButtonContainer = styled.div<{ - shouldDisplay: boolean; -}>` - display: flex; - align-items: center; - position: absolute; - justify-content: center; - flex-direction: column; - opacity: ${({ shouldDisplay }) => (shouldDisplay ? 1 : 0)}; - left: 50%; - bottom: 0; - transform: translateX(-50%) translateY(100%); -`; - -export const WorkflowDiagramStepNodeEditableContent = ({ - id, - data, - selected, - variant, - onDelete, - onClick, -}: { - id: string; - data: WorkflowDiagramStepNodeData; - variant: WorkflowDiagramNodeVariant; - selected: boolean; - onDelete: () => void; - onClick?: () => void; -}) => { - const [isHovered, setIsHovered] = useState(false); - - const handleMouseEnter = () => setIsHovered(true); - - const handleMouseLeave = () => { - setIsHovered(false); - }; - - const { isNodeCreationStarted } = useStartNodeCreation(); - - return ( - } - RightFloatingElement={ - selected && ( - - - - ) - } - BottomHoverFloatingElement={ - !data.hasNextStepIds && ( - - - - ) - } - /> - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon.tsx deleted file mode 100644 index d1c9c1be51..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { type WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { assertUnreachable } from 'twenty-shared/utils'; -import { useIcons } from 'twenty-ui/display'; - -const StyledStepNodeLabelIconContainer = styled.div` - align-items: center; - background: ${({ theme }) => theme.background.transparent.light}; - border-radius: ${({ theme }) => theme.spacing(1)}; - display: flex; - justify-content: center; - padding: ${({ theme }) => theme.spacing(1)}; -`; - -export const WorkflowDiagramStepNodeIcon = ({ - data, -}: { - data: WorkflowDiagramStepNodeData; -}) => { - const theme = useTheme(); - const { getIcon } = useIcons(); - const Icon = getIcon(getWorkflowNodeIconKey(data)); - - switch (data.nodeType) { - case 'trigger': { - switch (data.triggerType) { - case 'DATABASE_EVENT': - case 'MANUAL': - case 'CRON': - case 'WEBHOOK': { - return ( - - - - ); - } - } - - return assertUnreachable(data.triggerType); - } - case 'action': { - switch (data.actionType) { - case 'CODE': - case 'HTTP_REQUEST': { - return ( - - - - ); - } - case 'SEND_EMAIL': { - return ( - - - - ); - } - case 'AI_AGENT': { - return ( - - - - ); - } - default: { - return ( - - - - ); - } - } - } - } -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly.tsx deleted file mode 100644 index 5f40e8f994..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; -import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; -import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; -import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState'; -import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase'; -import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon'; -import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import { type WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { getNodeVariantFromStepRunStatus } from '@/workflow/workflow-diagram/utils/getNodeVariantFromStepRunStatus'; -import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; -import { useContext } from 'react'; -import { useSetRecoilState } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; -import { useIcons } from 'twenty-ui/display'; - -export const WorkflowDiagramStepNodeReadonly = ({ - id, - data, -}: { - id: string; - data: WorkflowDiagramStepNodeData; -}) => { - const { getIcon } = useIcons(); - - const workflowVisualizerWorkflowId = useRecoilComponentValue( - workflowVisualizerWorkflowIdComponentState, - ); - const workflowVisualizerWorkflowVersionId = useRecoilComponentValue( - workflowVisualizerWorkflowVersionIdComponentState, - ); - - const setWorkflowSelectedNode = useSetRecoilComponentState( - workflowSelectedNodeComponentState, - ); - - const { openWorkflowViewStepInCommandMenu } = useWorkflowCommandMenu(); - - const { isInRightDrawer } = useContext(ActionMenuContext); - - const setCommandMenuNavigationStack = useSetRecoilState( - commandMenuNavigationStackState, - ); - - return ( - } - displayHandle - onClick={() => { - if ( - !isDefined(workflowVisualizerWorkflowId) || - !isDefined(workflowVisualizerWorkflowVersionId) - ) { - throw new Error('Workflow ID and Version ID must be defined'); - } - - if (!isInRightDrawer) { - setCommandMenuNavigationStack([]); - } - - setWorkflowSelectedNode(id); - - openWorkflowViewStepInCommandMenu({ - workflowId: workflowVisualizerWorkflowId, - workflowVersionId: workflowVisualizerWorkflowVersionId, - title: data.name, - icon: getIcon(getWorkflowNodeIconKey(data)), - }); - }} - /> - ); -}; 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 index fa6deab9be..69dd23c8a5 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge.tsx @@ -1,67 +1,27 @@ -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 { type Theme, useTheme } from '@emotion/react'; -import { BaseEdge } from '@xyflow/react'; -import { isDefined } from 'twenty-shared/utils'; -import { StepStatus } from 'twenty-shared/workflow'; +import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { useTheme } from '@emotion/react'; +import { BaseEdge, type EdgeProps } from '@xyflow/react'; -const toMarkerId = (id: string) => `url(#${id})`; - -const getMarkerStart = (edgeExecutionStatus: StepStatus | undefined) => { - if (edgeExecutionStatus === StepStatus.SUCCESS) { - return EDGE_GREEN_CIRCLE_MARKED_ID; - } - - return undefined; -}; - -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; +type WorkflowRunDiagramBaseEdgeProps = Pick< + EdgeProps, + 'markerStart' | 'markerEnd' +> & { + path: string; }; export const WorkflowRunDiagramBaseEdge = ({ - edgePath, - edgeExecutionStatus, + path, + markerStart, + markerEnd, }: WorkflowRunDiagramBaseEdgeProps) => { const theme = useTheme(); - const markerStart = getMarkerStart(edgeExecutionStatus); - 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 632020bb5b..80aae2ee61 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 @@ -3,7 +3,7 @@ import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/component 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 { WorkflowRunDiagramStepNode } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramStepNode'; +import { WorkflowRunDiagramStepNode } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowRunDiagramStepNode'; import { getWorkflowRunStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps'; import { ReactFlowProvider } from '@xyflow/react'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramStepNode.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramStepNode.tsx deleted file mode 100644 index 6883096cb3..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramStepNode.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; -import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; -import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; -import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow'; -import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase'; -import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon'; -import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import { type WorkflowRunDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { getNodeVariantFromStepRunStatus } from '@/workflow/workflow-diagram/utils/getNodeVariantFromStepRunStatus'; -import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; -import { useContext } from 'react'; -import { useSetRecoilState } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; -import { useIcons } from 'twenty-ui/display'; - -export const WorkflowRunDiagramStepNode = ({ - id, - data, -}: { - id: string; - data: WorkflowRunDiagramStepNodeData; -}) => { - const { getIcon } = useIcons(); - - const workflowId = useRecoilComponentValue( - workflowVisualizerWorkflowIdComponentState, - ); - const workflowRunId = useWorkflowRunIdOrThrow(); - - const setWorkflowSelectedNode = useSetRecoilComponentState( - workflowSelectedNodeComponentState, - ); - - const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu(); - - const { isInRightDrawer } = useContext(ActionMenuContext); - - const setCommandMenuNavigationStack = useSetRecoilState( - commandMenuNavigationStackState, - ); - - return ( - } - displayHandle={false} - onClick={() => { - if (!isDefined(workflowId)) { - throw new Error('Workflow ID must be defined'); - } - - if (!isInRightDrawer) { - setCommandMenuNavigationStack([]); - } - - setWorkflowSelectedNode(id); - - openWorkflowRunViewStepInCommandMenu({ - workflowId, - workflowRunId, - title: data.name, - icon: getIcon(getWorkflowNodeIconKey(data)), - workflowSelectedNode: id, - stepExecutionStatus: data.runStatus, - }); - }} - /> - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEmptyTriggerEditable.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEmptyTriggerEditable.stories.tsx index 995fd74c57..62ab3e11a2 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEmptyTriggerEditable.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEmptyTriggerEditable.stories.tsx @@ -6,16 +6,11 @@ import { RecoilRoot } from 'recoil'; import { ComponentDecorator } from 'twenty-ui/testing'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator'; -import { WorkflowDiagramEmptyTriggerEditable } from '../WorkflowDiagramEmptyTriggerEditable'; +import { WorkflowDiagramEmptyTriggerEditable } from '../../workflow-nodes/components/WorkflowDiagramEmptyTriggerEditable'; const meta: Meta = { title: 'Modules/Workflow/WorkflowDiagramEmptyTriggerEditable', component: WorkflowDiagramEmptyTriggerEditable, - args: { - data: { - nodeType: 'empty-trigger', - }, - }, decorators: [I18nFrontDecorator], }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx index bc06cb748c..f52132d506 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx @@ -9,7 +9,7 @@ import { RecoilRoot } from 'recoil'; import { CatalogDecorator, type CatalogStory } from 'twenty-ui/testing'; import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; -import { WorkflowDiagramStepNodeEditableContent } from '../WorkflowDiagramStepNodeEditableContent'; +import { WorkflowDiagramStepNodeEditableContent } from '../../workflow-nodes/components/WorkflowDiagramStepNodeEditableContent'; const meta: Meta = { title: 'Modules/Workflow/WorkflowDiagramStepNodeEditableContent', diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/EdgeGreenCircleMarkedId.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/EdgeGreenCircleMarkedId.ts deleted file mode 100644 index e0329325c7..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/EdgeGreenCircleMarkedId.ts +++ /dev/null @@ -1 +0,0 @@ -export const EDGE_GREEN_CIRCLE_MARKED_ID = 'workflow-edge-green-circle'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerId.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerId.ts deleted file mode 100644 index c8a855e81b..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerId.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const EDGE_GREEN_ROUNDED_ARROW_MARKER_ID = - 'workflow-edge-green-arrow-rounded'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerWidthPx.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerWidthPx.ts deleted file mode 100644 index 20a2b0e341..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerWidthPx.ts +++ /dev/null @@ -1 +0,0 @@ -export const EDGE_GREEN_ROUNDED_ARROW_MARKER_WIDTH_PX = 6; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/EdgeRoundedArrowMarkerId.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/EdgeRoundedArrowMarkerId.ts deleted file mode 100644 index 4799e2c3de..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/EdgeRoundedArrowMarkerId.ts +++ /dev/null @@ -1 +0,0 @@ -export const EDGE_ROUNDED_ARROW_MARKER_ID = 'workflow-edge-arrow-rounded'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/NodeBorderWidth.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/NodeBorderWidth.ts deleted file mode 100644 index da353c77b0..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/NodeBorderWidth.ts +++ /dev/null @@ -1 +0,0 @@ -export const NODE_BORDER_WIDTH = 1; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerEditable.tsx new file mode 100644 index 0000000000..014b198adb --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerEditable.tsx @@ -0,0 +1,97 @@ +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId'; +import { WorkflowNodeContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer'; +import { WorkflowNodeIconContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer'; +import { WorkflowNodeLabel } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabel'; +import { WorkflowNodeLabelWithCounterPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabelWithCounterPart'; +import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart'; +import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle'; +import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; +import { useContext } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +const StyledNodeContainer = styled(WorkflowNodeContainer)` + border-color: ${({ theme }) => theme.border.color.strong}; + background: ${({ theme }) => theme.background.secondary}; + + &:hover { + background: linear-gradient( + 0deg, + ${({ theme }) => theme.background.transparent.lighter} 0%, + ${({ theme }) => theme.background.transparent.lighter} 100% + ), + ${({ theme }) => theme.background.secondary}; + } + + .selected & { + border-color: ${({ theme }) => theme.color.blue}; + background: ${({ theme }) => theme.adaptiveColors.blue1}; + } +`; + +const StyledNodeLabel = styled(WorkflowNodeLabel)` + color: ${({ theme }) => theme.font.color.tertiary}; + + .selected & { + color: ${({ theme }) => theme.tag.text.blue}; + } +`; + +const StyledNodeTitle = styled(WorkflowNodeTitle)` + color: ${({ theme }) => theme.font.color.light}; + + .selected & { + color: ${({ theme }) => theme.font.color.primary}; + } +`; + +export const WorkflowDiagramEmptyTriggerEditable = () => { + const { t } = useLingui(); + + const { openWorkflowTriggerTypeInCommandMenu } = useWorkflowCommandMenu(); + + const workflowVisualizerWorkflowId = useRecoilComponentValue( + workflowVisualizerWorkflowIdComponentState, + ); + + const { isInRightDrawer } = useContext(ActionMenuContext); + + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + + const handleClick = () => { + if (!isInRightDrawer) { + setCommandMenuNavigationStack([]); + } + + if (!isDefined(workflowVisualizerWorkflowId)) { + return; + } + + openWorkflowTriggerTypeInCommandMenu(workflowVisualizerWorkflowId); + }; + + return ( + + + + + + {t`Trigger`} + + + {t`Add a Trigger`} + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerReadonly.tsx new file mode 100644 index 0000000000..c067921b86 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerReadonly.tsx @@ -0,0 +1,122 @@ +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState'; +import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId'; +import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; +import { WorkflowNodeContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer'; +import { WorkflowNodeIconContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer'; +import { WorkflowNodeLabel } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabel'; +import { WorkflowNodeLabelWithCounterPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabelWithCounterPart'; +import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart'; +import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle'; +import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; +import { useContext } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { TRIGGER_STEP_ID } from 'twenty-shared/workflow'; +import { useIcons } from 'twenty-ui/display'; + +const StyledNodeContainer = styled(WorkflowNodeContainer)` + border-color: ${({ theme }) => theme.border.color.strong}; + background: ${({ theme }) => theme.background.secondary}; + + &:hover { + background: linear-gradient( + 0deg, + ${({ theme }) => theme.background.transparent.lighter} 0%, + ${({ theme }) => theme.background.transparent.lighter} 100% + ), + ${({ theme }) => theme.background.secondary}; + } + + .selected & { + border-color: ${({ theme }) => theme.color.blue}; + background: ${({ theme }) => theme.adaptiveColors.blue1}; + } +`; + +const StyledNodeLabel = styled(WorkflowNodeLabel)` + color: ${({ theme }) => theme.font.color.tertiary}; + + .selected & { + color: ${({ theme }) => theme.tag.text.blue}; + } +`; + +const StyledNodeTitle = styled(WorkflowNodeTitle)` + color: ${({ theme }) => theme.font.color.light}; + + .selected & { + color: ${({ theme }) => theme.font.color.primary}; + } +`; + +export const WorkflowDiagramEmptyTriggerReadonly = () => { + const { getIcon } = useIcons(); + const { t } = useLingui(); + + const workflowVisualizerWorkflowId = useRecoilComponentValue( + workflowVisualizerWorkflowIdComponentState, + ); + const workflowVisualizerWorkflowVersionId = useRecoilComponentValue( + workflowVisualizerWorkflowVersionIdComponentState, + ); + + const { isInRightDrawer } = useContext(ActionMenuContext); + + const { openWorkflowViewStepInCommandMenu } = useWorkflowCommandMenu(); + + const setWorkflowSelectedNode = useSetRecoilComponentState( + workflowSelectedNodeComponentState, + ); + + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + + const handleClick = () => { + if ( + !isDefined(workflowVisualizerWorkflowId) || + !isDefined(workflowVisualizerWorkflowVersionId) + ) { + throw new Error( + 'Workflow ID and Version ID must be defined to open the command menu.', + ); + } + + if (!isInRightDrawer) { + setCommandMenuNavigationStack([]); + } + + setWorkflowSelectedNode(TRIGGER_STEP_ID); + + openWorkflowViewStepInCommandMenu({ + workflowId: workflowVisualizerWorkflowId, + workflowVersionId: workflowVisualizerWorkflowVersionId, + title: t`Add a Trigger`, + icon: getIcon(null), + }); + }; + + return ( + + + + + + {t`Trigger`} + + + {t`Add a Trigger`} + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramFilterEdgeEditable.tsx similarity index 100% rename from packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeEditable.tsx rename to packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramFilterEdgeEditable.tsx diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramFilterEdgeReadonly.tsx similarity index 100% rename from packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeReadonly.tsx rename to packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramFilterEdgeReadonly.tsx diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleEditable.tsx new file mode 100644 index 0000000000..71327f3d2d --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleEditable.tsx @@ -0,0 +1,95 @@ +import { NODE_HANDLE_HEIGHT_PX } from '@/workflow/workflow-diagram/constants/NodeHandleHeightPx'; +import { NODE_HANDLE_WIDTH_PX } from '@/workflow/workflow-diagram/constants/NodeHandleWidthPx'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import { Handle, type HandleProps } from '@xyflow/react'; +import { FeatureFlagKey } from '~/generated/graphql'; + +type WorkflowDiagramHandleEditableProps = HandleProps & { + selected: boolean; +}; + +const HANDLE_SCALE_ON_HOVER = 1.5; + +const StyledHandle = styled(Handle, { + shouldForwardProp: (prop) => + prop !== 'disableHoverEffect' && prop !== 'selected', +})<{ + disableHoverEffect: boolean; + selected: boolean; +}>` + &.react-flow__handle { + height: ${NODE_HANDLE_HEIGHT_PX}px; + width: ${NODE_HANDLE_WIDTH_PX}px; + border-color: ${({ theme, selected }) => + selected ? theme.color.blue : theme.border.color.strong}; + background: ${({ theme, selected }) => + selected ? theme.adaptiveColors.blue1 : theme.background.primary}; + transition: + transform 0.1s ease-out, + background 0.1s, + border-color 0.1s; + z-index: 1; + + ${({ position }) => { + if (position === 'top') { + return css` + transform: translate(-50%, -50%); + transform-origin: top left; + `; + } else if (position === 'bottom') { + return css` + transform: translate(-50%, 50%); + transform-origin: bottom left; + `; + } + }} + + &.connectionindicator { + cursor: pointer; + } + + ${({ disableHoverEffect, theme, position }) => { + if (disableHoverEffect) { + return undefined; + } + + return css` + &:hover { + background: ${theme.adaptiveColors.blue1} !important; + border-color: ${theme.color.blue} !important; + + ${position === 'top' && + css` + transform: scale(${HANDLE_SCALE_ON_HOVER}) translate(-50%, -50%); + `} + + ${position === 'bottom' && + css` + transform: scale(${HANDLE_SCALE_ON_HOVER}) translate(-50%, 50%); + `} + } + `; + }} + } +`; + +export const WorkflowDiagramHandleEditable = ({ + type, + position, + selected, +}: WorkflowDiagramHandleEditableProps) => { + const isWorkflowBranchEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED, + ); + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleReadonly.tsx new file mode 100644 index 0000000000..ded964f1a2 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleReadonly.tsx @@ -0,0 +1,53 @@ +import { NODE_HANDLE_HEIGHT_PX } from '@/workflow/workflow-diagram/constants/NodeHandleHeightPx'; +import { NODE_HANDLE_WIDTH_PX } from '@/workflow/workflow-diagram/constants/NodeHandleWidthPx'; +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import { Handle, type HandleProps } from '@xyflow/react'; + +type WorkflowDiagramHandleReadonlyProps = HandleProps & { + selected: boolean; +}; + +const StyledHandle = styled(Handle)<{ + selected: boolean; +}>` + &.react-flow__handle { + height: ${NODE_HANDLE_HEIGHT_PX}px; + width: ${NODE_HANDLE_WIDTH_PX}px; + border-color: ${({ theme, selected }) => + selected ? theme.color.blue : theme.border.color.strong}; + background: ${({ theme, selected }) => + selected ? theme.adaptiveColors.blue1 : theme.background.primary}; + transition: + transform 0.1s ease-out, + background 0.1s, + border-color 0.1s; + z-index: 1; + + ${({ position }) => { + if (position === 'top') { + return css` + transform: translate(-50%, -50%); + transform-origin: top left; + `; + } else if (position === 'bottom') { + return css` + transform: translate(-50%, 50%); + transform-origin: bottom left; + `; + } + }} + + &.connectionindicator { + cursor: default; + } + } +`; + +export const WorkflowDiagramHandleReadonly = ({ + type, + position, + selected, +}: WorkflowDiagramHandleReadonlyProps) => { + return ; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditable.tsx similarity index 96% rename from packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable.tsx rename to packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditable.tsx index 1559eca3a5..264c2de051 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditable.tsx @@ -4,10 +4,10 @@ import { commandMenuNavigationStackState } from '@/command-menu/states/commandMe import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { WorkflowDiagramStepNodeEditableContent } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent'; import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; import { type WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; +import { WorkflowDiagramStepNodeEditableContent } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditableContent'; import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep'; import { useContext } from 'react'; import { useSetRecoilState } from 'recoil'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditableContent.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditableContent.tsx new file mode 100644 index 0000000000..fa55fdea94 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditableContent.tsx @@ -0,0 +1,167 @@ +import { WorkflowDiagramCreateStepElement } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement'; +import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId'; +import { useEdgeSelected } from '@/workflow/workflow-diagram/hooks/useEdgeSelected'; +import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; +import { type WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { type WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant'; +import { WorkflowDiagramHandleEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleEditable'; +import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon'; +import { WorkflowNodeContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer'; +import { WorkflowNodeIconContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer'; +import { WorkflowNodeLabel } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabel'; +import { WorkflowNodeLabelWithCounterPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabelWithCounterPart'; +import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart'; +import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle'; +import styled from '@emotion/styled'; +import { Position } from '@xyflow/react'; +import { useState } from 'react'; +import { capitalize } from 'twenty-shared/utils'; +import { IconTrash } from 'twenty-ui/display'; +import { FloatingIconButton } from 'twenty-ui/input'; + +const StyledAddStepButtonContainer = styled.div<{ + shouldDisplay: boolean; +}>` + display: flex; + align-items: center; + position: absolute; + justify-content: center; + flex-direction: column; + opacity: ${({ shouldDisplay }) => (shouldDisplay ? 1 : 0)}; + left: 50%; + bottom: 0; + transform: translateX(-50%) translateY(100%); +`; + +const StyledNodeContainer = styled(WorkflowNodeContainer)` + border-color: ${({ theme }) => theme.border.color.strong}; + background: ${({ theme }) => theme.background.secondary}; + + &:hover { + background: linear-gradient( + 0deg, + ${({ theme }) => theme.background.transparent.lighter} 0%, + ${({ theme }) => theme.background.transparent.lighter} 100% + ), + ${({ theme }) => theme.background.secondary}; + } + + .selected & { + border-color: ${({ theme }) => theme.color.blue}; + background: ${({ theme }) => theme.adaptiveColors.blue1}; + } +`; + +const StyledNodeLabel = styled(WorkflowNodeLabel)` + color: ${({ theme }) => theme.font.color.tertiary}; + + .selected & { + color: ${({ theme }) => theme.tag.text.blue}; + } +`; + +const StyledNodeTitle = styled(WorkflowNodeTitle)` + color: ${({ theme }) => theme.font.color.primary}; +`; + +const StyledDeleteButtonContainer = styled.div` + display: flex; + align-items: center; + position: absolute; + right: ${({ theme }) => theme.spacing(-4)}; + bottom: 0; + top: 0; + transform: translateX(100%); +`; + +export const WorkflowDiagramStepNodeEditableContent = ({ + id, + data, + selected, + onClick, + onDelete, +}: { + id: string; + data: WorkflowDiagramStepNodeData; + variant: WorkflowDiagramNodeVariant; + selected: boolean; + onDelete: () => void; + onClick?: () => void; +}) => { + const [isHovered, setIsHovered] = useState(false); + + const handleMouseEnter = () => { + setIsHovered(true); + }; + + const handleMouseLeave = () => { + setIsHovered(false); + }; + + const { isNodeCreationStarted } = useStartNodeCreation(); + + const { getNodeHandlesSelectedState } = useEdgeSelected(); + + const handlesSelectedState = getNodeHandlesSelectedState(id); + + return ( + <> + {data.nodeType !== 'trigger' && ( + + )} + + + + + + + + + {capitalize(data.nodeType)} + + + {data.name} + + + {selected && ( + + + + )} + + + {!data.hasNextStepIds && ( + + + + )} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon.tsx new file mode 100644 index 0000000000..e59ce72c13 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon.tsx @@ -0,0 +1,61 @@ +import { type WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; +import { useTheme } from '@emotion/react'; +import { assertUnreachable } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; + +export const WorkflowDiagramStepNodeIcon = ({ + data, +}: { + data: WorkflowDiagramStepNodeData; +}) => { + const theme = useTheme(); + const { getIcon } = useIcons(); + const Icon = getIcon(getWorkflowNodeIconKey(data)); + + switch (data.nodeType) { + case 'trigger': { + switch (data.triggerType) { + case 'DATABASE_EVENT': + case 'MANUAL': + case 'CRON': + case 'WEBHOOK': { + return ( + + ); + } + } + + return assertUnreachable(data.triggerType); + } + case 'action': { + switch (data.actionType) { + case 'CODE': + case 'HTTP_REQUEST': { + return ( + + ); + } + case 'SEND_EMAIL': { + return ; + } + case 'AI_AGENT': { + return ; + } + default: { + return ( + + ); + } + } + } + } +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeReadonly.tsx new file mode 100644 index 0000000000..00d097ded4 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeReadonly.tsx @@ -0,0 +1,144 @@ +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState'; +import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId'; +import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; +import { type WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; +import { WorkflowDiagramHandleReadonly } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleReadonly'; +import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon'; +import { WorkflowNodeContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer'; +import { WorkflowNodeIconContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer'; +import { WorkflowNodeLabel } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabel'; +import { WorkflowNodeLabelWithCounterPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabelWithCounterPart'; +import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart'; +import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle'; +import styled from '@emotion/styled'; +import { Position } from '@xyflow/react'; +import { useContext } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { capitalize, isDefined } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; + +const StyledNodeContainer = styled(WorkflowNodeContainer)` + border-color: ${({ theme }) => theme.border.color.strong}; + background: ${({ theme }) => theme.background.secondary}; + + &:hover { + background: linear-gradient( + 0deg, + ${({ theme }) => theme.background.transparent.lighter} 0%, + ${({ theme }) => theme.background.transparent.lighter} 100% + ), + ${({ theme }) => theme.background.secondary}; + } + + .selected & { + border-color: ${({ theme }) => theme.color.blue}; + background: ${({ theme }) => theme.adaptiveColors.blue1}; + } +`; + +const StyledNodeLabel = styled(WorkflowNodeLabel)` + color: ${({ theme }) => theme.font.color.tertiary}; + + .selected & { + color: ${({ theme }) => theme.tag.text.blue}; + } +`; + +const StyledNodeTitle = styled(WorkflowNodeTitle)` + color: ${({ theme }) => theme.font.color.primary}; +`; + +export const WorkflowDiagramStepNodeReadonly = ({ + id, + selected, + data, +}: { + id: string; + selected: boolean; + data: WorkflowDiagramStepNodeData; +}) => { + const { getIcon } = useIcons(); + + const workflowVisualizerWorkflowId = useRecoilComponentValue( + workflowVisualizerWorkflowIdComponentState, + ); + const workflowVisualizerWorkflowVersionId = useRecoilComponentValue( + workflowVisualizerWorkflowVersionIdComponentState, + ); + + const setWorkflowSelectedNode = useSetRecoilComponentState( + workflowSelectedNodeComponentState, + ); + + const { openWorkflowViewStepInCommandMenu } = useWorkflowCommandMenu(); + + const { isInRightDrawer } = useContext(ActionMenuContext); + + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + + const handleClick = () => { + if ( + !isDefined(workflowVisualizerWorkflowId) || + !isDefined(workflowVisualizerWorkflowVersionId) + ) { + throw new Error('Workflow ID and Version ID must be defined'); + } + + if (!isInRightDrawer) { + setCommandMenuNavigationStack([]); + } + + setWorkflowSelectedNode(id); + + openWorkflowViewStepInCommandMenu({ + workflowId: workflowVisualizerWorkflowId, + workflowVersionId: workflowVisualizerWorkflowVersionId, + title: data.name, + icon: getIcon(getWorkflowNodeIconKey(data)), + }); + }; + + return ( + <> + {data.nodeType !== 'trigger' && ( + + )} + + + + + + + + + {capitalize(data.nodeType)} + + + {data.name} + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer.tsx new file mode 100644 index 0000000000..259b2f02ee --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer.tsx @@ -0,0 +1,18 @@ +import styled from '@emotion/styled'; + +const StyledNodeContainer = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + max-width: 240px; + min-width: 44px; + padding: ${({ theme }) => theme.spacing(2)}; + border-radius: ${({ theme }) => theme.border.radius.md}; + border-width: 1px; + border-style: solid; + box-sizing: border-box; + cursor: pointer; + position: relative; +`; + +export { StyledNodeContainer as WorkflowNodeContainer }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer.tsx new file mode 100644 index 0000000000..35232724fb --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer.tsx @@ -0,0 +1,14 @@ +import styled from '@emotion/styled'; + +const StyledNodeIconContainer = styled.div` + align-items: center; + background: ${({ theme }) => theme.background.transparent.light}; + border-radius: 4px; + box-sizing: border-box; + display: flex; + height: 32px; + justify-content: center; + width: 32px; +`; + +export { StyledNodeIconContainer as WorkflowNodeIconContainer }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabel.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabel.tsx new file mode 100644 index 0000000000..8c2a482cd7 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabel.tsx @@ -0,0 +1,9 @@ +import styled from '@emotion/styled'; +import { Label } from 'twenty-ui/display'; + +const StyledNodeLabel = styled(Label)` + box-sizing: border-box; + flex: 1 0 0; +`; + +export { StyledNodeLabel as WorkflowNodeLabel }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabelWithCounterPart.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabelWithCounterPart.tsx new file mode 100644 index 0000000000..844abc38df --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabelWithCounterPart.tsx @@ -0,0 +1,12 @@ +import styled from '@emotion/styled'; + +const StyledNodeLabelWithCounterPart = styled.div` + align-items: center; + align-self: stretch; + display: flex; + height: 14px; + justify-content: space-between; + box-sizing: border-box; +`; + +export { StyledNodeLabelWithCounterPart as WorkflowNodeLabelWithCounterPart }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart.tsx new file mode 100644 index 0000000000..aa67680d60 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart.tsx @@ -0,0 +1,13 @@ +import styled from '@emotion/styled'; + +const StyledNodeRightPart = styled.div` + align-items: flex-start; + align-self: stretch; + display: flex; + flex-direction: column; + justify-content: space-between; + max-width: 184px; + box-sizing: border-box; +`; + +export { StyledNodeRightPart as WorkflowNodeRightPart }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle.tsx new file mode 100644 index 0000000000..58fabb06fb --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle.tsx @@ -0,0 +1,15 @@ +import styled from '@emotion/styled'; + +const StyledNodeTitle = styled.div` + box-sizing: border-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + align-self: stretch; + display: -webkit-box; + font-size: ${({ theme }) => theme.font.size.md}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + overflow: hidden; + text-overflow: ellipsis; +`; + +export { StyledNodeTitle as WorkflowNodeTitle }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowRunDiagramStepNode.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowRunDiagramStepNode.tsx new file mode 100644 index 0000000000..ca6f2db2bd --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowRunDiagramStepNode.tsx @@ -0,0 +1,240 @@ +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow'; +import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId'; +import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; +import { type WorkflowRunDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; +import { WorkflowDiagramHandleReadonly } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleReadonly'; +import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon'; +import { WorkflowNodeContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer'; +import { WorkflowNodeIconContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer'; +import { WorkflowNodeLabel } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabel'; +import { WorkflowNodeLabelWithCounterPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabelWithCounterPart'; +import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart'; +import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { Position } from '@xyflow/react'; +import { useContext } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { capitalize, isDefined } from 'twenty-shared/utils'; +import { StepStatus } from 'twenty-shared/workflow'; +import { IconCheck, IconX, useIcons } from 'twenty-ui/display'; +import { Loader } from 'twenty-ui/feedback'; + +const StyledNodeContainer = styled(WorkflowNodeContainer)` + border-color: ${({ theme }) => theme.border.color.strong}; + background: ${({ theme }) => theme.background.secondary}; + + &:hover { + background: linear-gradient( + 0deg, + ${({ theme }) => theme.background.transparent.lighter} 0%, + ${({ theme }) => theme.background.transparent.lighter} 100% + ), + ${({ theme }) => theme.background.secondary}; + } + + .selected & { + border-color: ${({ theme }) => theme.color.blue}; + background: ${({ theme }) => theme.adaptiveColors.blue1}; + } + + .selected &[data-status='RUNNING'], + .selected &[data-status='PENDING'] { + border-color: ${({ theme }) => theme.color.yellow}; + background: ${({ theme }) => theme.adaptiveColors.yellow1}; + } + + .selected &[data-status='FAILED'] { + border-color: ${({ theme }) => theme.color.red}; + background: ${({ theme }) => theme.adaptiveColors.red1}; + } + + .selected &[data-status='SUCCESS'] { + border-color: ${({ theme }) => theme.color.turquoise}; + background: ${({ theme }) => theme.adaptiveColors.turquoise1}; + } +`; + +const StyledNodeLabelWithCounterPart = styled(WorkflowNodeLabelWithCounterPart)` + column-gap: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledNodeLabel = styled(WorkflowNodeLabel)` + color: ${({ theme }) => theme.font.color.tertiary}; + + .selected & { + color: ${({ theme }) => theme.tag.text.blue}; + } + + &[data-status='RUNNING'], + &[data-status='PENDING'] { + color: ${({ theme }) => theme.tag.text.yellow}; + } + + &[data-status='FAILED'] { + color: ${({ theme }) => theme.tag.text.red}; + } + + &[data-status='SUCCESS'] { + color: ${({ theme }) => theme.tag.text.green}; + } +`; + +const StyledNodeTitle = styled(WorkflowNodeTitle)` + color: ${({ theme }) => theme.font.color.light}; + + &[data-status='RUNNING'], + &[data-status='PENDING'], + &[data-status='FAILED'], + &[data-status='SUCCESS'], + .selected & { + color: ${({ theme }) => theme.font.color.primary}; + } +`; + +const StyledNodeCounter = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(1)}; + justify-content: flex-end; + box-sizing: border-box; +`; + +const StyledColorIcon = styled.div` + display: flex; + width: 14px; + height: 14px; + justify-content: center; + align-items: center; + box-sizing: border-box; + border-radius: ${({ theme }) => theme.border.radius.sm}; + + &[data-status='FAILED'] { + background: ${({ theme }) => theme.tag.background.red}; + } + + &[data-status='SUCCESS'] { + background: ${({ theme }) => theme.tag.background.turquoise}; + } +`; + +export const WorkflowRunDiagramStepNode = ({ + id, + data, + selected, +}: { + id: string; + data: WorkflowRunDiagramStepNodeData; + selected: boolean; +}) => { + const { getIcon } = useIcons(); + const theme = useTheme(); + + const workflowId = useRecoilComponentValue( + workflowVisualizerWorkflowIdComponentState, + ); + const workflowRunId = useWorkflowRunIdOrThrow(); + + const setWorkflowSelectedNode = useSetRecoilComponentState( + workflowSelectedNodeComponentState, + ); + + const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu(); + + const { isInRightDrawer } = useContext(ActionMenuContext); + + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + + const handleClick = () => { + if (!isDefined(workflowId)) { + throw new Error('Workflow ID must be defined'); + } + + if (!isInRightDrawer) { + setCommandMenuNavigationStack([]); + } + + setWorkflowSelectedNode(id); + + openWorkflowRunViewStepInCommandMenu({ + workflowId, + workflowRunId, + title: data.name, + icon: getIcon(getWorkflowNodeIconKey(data)), + workflowSelectedNode: id, + stepExecutionStatus: data.runStatus, + }); + }; + + return ( + <> + {data.nodeType !== 'trigger' && ( + + )} + + + + + + + + + + {capitalize(data.nodeType)} + + + {data.runStatus === StepStatus.SUCCESS && ( + + + + + + )} + + {data.runStatus === StepStatus.FAILED && ( + + + + + + )} + + {(data.runStatus === StepStatus.RUNNING || + data.runStatus === StepStatus.PENDING) && ( + + + + )} + + + + {data.name} + + + + + + + ); +};