Implement new workflow node design (#13929)
## Demo workflows + workflow runs https://github.com/user-attachments/assets/f2ee9451-37c3-4fce-803e-7696093456c6 ## Demo workflow versions https://github.com/user-attachments/assets/0f40c670-3fb3-4c73-a842-549b31ae26df ## With Branches disabled https://github.com/user-attachments/assets/a9a39515-dd18-4310-b96e-a42dbebb17d9
This commit is contained in:
committed by
GitHub
parent
abe6b7828b
commit
cded145609
-99
@@ -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),
|
||||
})<WorkflowDiagramBaseHandleProps>`
|
||||
// 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 (
|
||||
<StyledHandle
|
||||
type={type}
|
||||
position={position}
|
||||
style={{ opacity: isVisible ? 1 : 0 }}
|
||||
disableHoverEffect={!isWorkflowBranchEnabled}
|
||||
selected={selected}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-14
@@ -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;
|
||||
|
||||
+3
-3
@@ -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';
|
||||
|
||||
+3
-3
@@ -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 = ({
|
||||
|
||||
-49
@@ -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 (
|
||||
<svg style={{ position: 'absolute', top: 0, left: 0 }}>
|
||||
<defs>
|
||||
<marker
|
||||
id={EDGE_ROUNDED_ARROW_MARKER_ID}
|
||||
markerHeight={5}
|
||||
markerWidth={6}
|
||||
refX={3}
|
||||
refY={2.5}
|
||||
>
|
||||
<path
|
||||
d="M0.31094 1.1168C0.178029 0.917434 0.320947 0.650391 0.560555 0.650391H5.43945C5.67905 0.650391 5.82197 0.917434 5.68906 1.1168L3.62404 4.21433C3.32717 4.65963 2.67283 4.65963 2.37596 4.21433L0.31094 1.1168Z"
|
||||
fill={theme.border.color.strong}
|
||||
/>
|
||||
</marker>
|
||||
|
||||
<marker
|
||||
id={EDGE_GREEN_ROUNDED_ARROW_MARKER_ID}
|
||||
markerHeight={5}
|
||||
markerWidth={EDGE_GREEN_ROUNDED_ARROW_MARKER_WIDTH_PX}
|
||||
refX={EDGE_GREEN_ROUNDED_ARROW_MARKER_WIDTH_PX / 2}
|
||||
refY={2.5}
|
||||
>
|
||||
<path
|
||||
d="M0.31094 1.1168C0.178029 0.917434 0.320947 0.650391 0.560555 0.650391H5.43945C5.67905 0.650391 5.82197 0.917434 5.68906 1.1168L3.62404 4.21433C3.32717 4.65963 2.67283 4.65963 2.37596 4.21433L0.31094 1.1168Z"
|
||||
fill={theme.tag.text.turquoise}
|
||||
/>
|
||||
</marker>
|
||||
|
||||
<marker
|
||||
id={EDGE_BRANCH_ARROW_MARKER_ID}
|
||||
markerHeight={8}
|
||||
@@ -50,25 +20,6 @@ export const WorkflowDiagramCustomMarkers = () => {
|
||||
fill={theme.background.primary}
|
||||
/>
|
||||
</marker>
|
||||
|
||||
<marker
|
||||
markerHeight={5}
|
||||
markerWidth={5}
|
||||
refX={2}
|
||||
refY={2.5}
|
||||
id={EDGE_GREEN_CIRCLE_MARKED_ID}
|
||||
>
|
||||
<rect
|
||||
x={0.5}
|
||||
y={0.5}
|
||||
height={3}
|
||||
width={3}
|
||||
rx="1.5"
|
||||
fill="white"
|
||||
stroke={theme.tag.text.turquoise}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
+5
-3
@@ -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 (
|
||||
<WorkflowRunDiagramBaseEdge
|
||||
edgePath={edgePath}
|
||||
edgeExecutionStatus={data?.edgeExecutionStatus}
|
||||
path={edgePath}
|
||||
markerStart={markerStart}
|
||||
markerEnd={markerEnd}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
-57
@@ -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 (
|
||||
<WorkflowDiagramStepNodeBase
|
||||
id="trigger"
|
||||
name={t`Add a Trigger`}
|
||||
nodeType="trigger"
|
||||
variant="empty"
|
||||
Icon={<StyledStepNodeLabelIconContainer />}
|
||||
onClick={() => {
|
||||
if (!isInRightDrawer) {
|
||||
setCommandMenuNavigationStack([]);
|
||||
}
|
||||
|
||||
if (!isDefined(workflowVisualizerWorkflowId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
openWorkflowTriggerTypeInCommandMenu(workflowVisualizerWorkflowId);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-82
@@ -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 (
|
||||
<WorkflowDiagramStepNodeBase
|
||||
id="trigger"
|
||||
name={t`Add a Trigger`}
|
||||
nodeType="trigger"
|
||||
variant="empty"
|
||||
Icon={<StyledStepNodeLabelIconContainer />}
|
||||
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),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+5
-2
@@ -63,6 +63,8 @@ export const WorkflowDiagramFilterEdgeRun = ({
|
||||
targetX,
|
||||
targetY,
|
||||
data,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
}: WorkflowDiagramFilterEdgeRunProps) => {
|
||||
assertFilterEdgeDataOrThrow(data);
|
||||
|
||||
@@ -98,8 +100,9 @@ export const WorkflowDiagramFilterEdgeRun = ({
|
||||
return (
|
||||
<>
|
||||
<WorkflowRunDiagramBaseEdge
|
||||
edgePath={edgePath}
|
||||
edgeExecutionStatus={data.edgeExecutionStatus}
|
||||
path={edgePath}
|
||||
markerStart={markerStart}
|
||||
markerEnd={markerEnd}
|
||||
/>
|
||||
|
||||
<EdgeLabelRenderer>
|
||||
|
||||
+5
-3
@@ -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 (
|
||||
<WorkflowRunDiagramBaseEdge
|
||||
edgePath={edgePath}
|
||||
edgeExecutionStatus={data?.edgeExecutionStatus}
|
||||
path={edgePath}
|
||||
markerStart={markerStart}
|
||||
markerEnd={markerEnd}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
-216
@@ -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<HTMLDivElement>) => void;
|
||||
onMouseLeave?: (event: MouseEvent<HTMLDivElement>) => void;
|
||||
}) => {
|
||||
const { getNodeHandlesSelectedState } = useEdgeSelected();
|
||||
|
||||
const handlesSelectedState = getNodeHandlesSelectedState(id);
|
||||
|
||||
return (
|
||||
<StyledStepNodeContainer
|
||||
className="workflow-node-container"
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{nodeType !== 'trigger' && (
|
||||
<WorkflowDiagramBaseHandle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
isVisible={displayHandle}
|
||||
selected={handlesSelectedState.targetHandle}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StyledStepNodeType variant="small" nodeVariant={variant}>
|
||||
{capitalize(nodeType)}
|
||||
</StyledStepNodeType>
|
||||
|
||||
<StyledStepNodeInnerContainer variant={variant}>
|
||||
<StyledStepNodeLabel variant={variant}>
|
||||
{variant === 'running' ? <Loader /> : Icon}
|
||||
|
||||
<OverflowingTextWithTooltip text={name} hideTooltip={true} />
|
||||
</StyledStepNodeLabel>
|
||||
|
||||
{RightFloatingElement}
|
||||
|
||||
{BottomHoverFloatingElement}
|
||||
</StyledStepNodeInnerContainer>
|
||||
|
||||
<WorkflowDiagramBaseHandle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
isVisible={displayHandle}
|
||||
selected={handlesSelectedState.sourceHandle || selected}
|
||||
/>
|
||||
</StyledStepNodeContainer>
|
||||
);
|
||||
};
|
||||
-100
@@ -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 (
|
||||
<WorkflowDiagramStepNodeBase
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={onClick}
|
||||
id={id}
|
||||
name={data.name}
|
||||
variant={variant}
|
||||
selected={selected}
|
||||
nodeType={data.nodeType}
|
||||
Icon={<WorkflowDiagramStepNodeIcon data={data} />}
|
||||
RightFloatingElement={
|
||||
selected && (
|
||||
<StyledDeleteButtonContainer>
|
||||
<FloatingIconButton
|
||||
size="medium"
|
||||
Icon={IconTrash}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
</StyledDeleteButtonContainer>
|
||||
)
|
||||
}
|
||||
BottomHoverFloatingElement={
|
||||
!data.hasNextStepIds && (
|
||||
<StyledAddStepButtonContainer
|
||||
shouldDisplay={
|
||||
isHovered ||
|
||||
selected ||
|
||||
isNodeCreationStarted({ parentStepId: data.stepId })
|
||||
}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<WorkflowDiagramCreateStepElement data={data} />
|
||||
</StyledAddStepButtonContainer>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-88
@@ -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 (
|
||||
<StyledStepNodeLabelIconContainer>
|
||||
<Icon
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
</StyledStepNodeLabelIconContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return assertUnreachable(data.triggerType);
|
||||
}
|
||||
case 'action': {
|
||||
switch (data.actionType) {
|
||||
case 'CODE':
|
||||
case 'HTTP_REQUEST': {
|
||||
return (
|
||||
<StyledStepNodeLabelIconContainer>
|
||||
<Icon
|
||||
size={theme.icon.size.md}
|
||||
color={theme.color.orange}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledStepNodeLabelIconContainer>
|
||||
);
|
||||
}
|
||||
case 'SEND_EMAIL': {
|
||||
return (
|
||||
<StyledStepNodeLabelIconContainer>
|
||||
<Icon size={theme.icon.size.md} color={theme.color.blue} />
|
||||
</StyledStepNodeLabelIconContainer>
|
||||
);
|
||||
}
|
||||
case 'AI_AGENT': {
|
||||
return (
|
||||
<StyledStepNodeLabelIconContainer>
|
||||
<Icon size={theme.icon.size.md} color={theme.color.pink} />
|
||||
</StyledStepNodeLabelIconContainer>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<StyledStepNodeLabelIconContainer>
|
||||
<Icon
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledStepNodeLabelIconContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
-78
@@ -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 (
|
||||
<WorkflowDiagramStepNodeBase
|
||||
id={id}
|
||||
name={data.name}
|
||||
variant={getNodeVariantFromStepRunStatus(data.runStatus)}
|
||||
nodeType={data.nodeType}
|
||||
Icon={<WorkflowDiagramStepNodeIcon data={data} />}
|
||||
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)),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+15
-55
@@ -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<WorkflowDiagramEdge>,
|
||||
'markerStart' | 'markerEnd'
|
||||
> & {
|
||||
path: string;
|
||||
};
|
||||
|
||||
export const WorkflowRunDiagramBaseEdge = ({
|
||||
edgePath,
|
||||
edgeExecutionStatus,
|
||||
path,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
}: WorkflowRunDiagramBaseEdgeProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const markerStart = getMarkerStart(edgeExecutionStatus);
|
||||
|
||||
return (
|
||||
<BaseEdge
|
||||
markerStart={isDefined(markerStart) ? toMarkerId(markerStart) : undefined}
|
||||
markerEnd={toMarkerId(getMarkerEnd(edgeExecutionStatus))}
|
||||
path={edgePath}
|
||||
style={{
|
||||
stroke: getStrokeColor({
|
||||
theme,
|
||||
edgeExecutionStatus,
|
||||
}),
|
||||
}}
|
||||
markerStart={markerStart}
|
||||
markerEnd={markerEnd}
|
||||
path={path}
|
||||
style={{ stroke: theme.border.color.strong }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
|
||||
-75
@@ -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 (
|
||||
<WorkflowDiagramStepNodeBase
|
||||
id={id}
|
||||
name={data.name}
|
||||
variant={getNodeVariantFromStepRunStatus(data.runStatus)}
|
||||
nodeType={data.nodeType}
|
||||
Icon={<WorkflowDiagramStepNodeIcon data={data} />}
|
||||
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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+1
-6
@@ -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<typeof WorkflowDiagramEmptyTriggerEditable> = {
|
||||
title: 'Modules/Workflow/WorkflowDiagramEmptyTriggerEditable',
|
||||
component: WorkflowDiagramEmptyTriggerEditable,
|
||||
args: {
|
||||
data: {
|
||||
nodeType: 'empty-trigger',
|
||||
},
|
||||
},
|
||||
decorators: [I18nFrontDecorator],
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -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<typeof WorkflowDiagramStepNodeEditableContent> = {
|
||||
title: 'Modules/Workflow/WorkflowDiagramStepNodeEditableContent',
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const EDGE_GREEN_CIRCLE_MARKED_ID = 'workflow-edge-green-circle';
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
export const EDGE_GREEN_ROUNDED_ARROW_MARKER_ID =
|
||||
'workflow-edge-green-arrow-rounded';
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const EDGE_GREEN_ROUNDED_ARROW_MARKER_WIDTH_PX = 6;
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const EDGE_ROUNDED_ARROW_MARKER_ID = 'workflow-edge-arrow-rounded';
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const NODE_BORDER_WIDTH = 1;
|
||||
+97
@@ -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 (
|
||||
<StyledNodeContainer
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<WorkflowNodeIconContainer />
|
||||
|
||||
<WorkflowNodeRightPart>
|
||||
<WorkflowNodeLabelWithCounterPart>
|
||||
<StyledNodeLabel>{t`Trigger`}</StyledNodeLabel>
|
||||
</WorkflowNodeLabelWithCounterPart>
|
||||
|
||||
<StyledNodeTitle>{t`Add a Trigger`}</StyledNodeTitle>
|
||||
</WorkflowNodeRightPart>
|
||||
</StyledNodeContainer>
|
||||
);
|
||||
};
|
||||
+122
@@ -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 (
|
||||
<StyledNodeContainer
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<WorkflowNodeIconContainer />
|
||||
|
||||
<WorkflowNodeRightPart>
|
||||
<WorkflowNodeLabelWithCounterPart>
|
||||
<StyledNodeLabel>{t`Trigger`}</StyledNodeLabel>
|
||||
</WorkflowNodeLabelWithCounterPart>
|
||||
|
||||
<StyledNodeTitle>{t`Add a Trigger`}</StyledNodeTitle>
|
||||
</WorkflowNodeRightPart>
|
||||
</StyledNodeContainer>
|
||||
);
|
||||
};
|
||||
+95
@@ -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 (
|
||||
<StyledHandle
|
||||
type={type}
|
||||
position={position}
|
||||
disableHoverEffect={!isWorkflowBranchEnabled}
|
||||
selected={selected}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+53
@@ -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 <StyledHandle type={type} position={position} selected={selected} />;
|
||||
};
|
||||
+1
-1
@@ -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';
|
||||
+167
@@ -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' && (
|
||||
<WorkflowDiagramHandleEditable
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
selected={handlesSelectedState.targetHandle}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StyledNodeContainer
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID}
|
||||
onClick={onClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<WorkflowNodeIconContainer>
|
||||
<WorkflowDiagramStepNodeIcon data={data} />
|
||||
</WorkflowNodeIconContainer>
|
||||
|
||||
<WorkflowNodeRightPart>
|
||||
<WorkflowNodeLabelWithCounterPart>
|
||||
<StyledNodeLabel>{capitalize(data.nodeType)}</StyledNodeLabel>
|
||||
</WorkflowNodeLabelWithCounterPart>
|
||||
|
||||
<StyledNodeTitle>{data.name}</StyledNodeTitle>
|
||||
</WorkflowNodeRightPart>
|
||||
|
||||
{selected && (
|
||||
<StyledDeleteButtonContainer>
|
||||
<FloatingIconButton
|
||||
size="medium"
|
||||
Icon={IconTrash}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
</StyledDeleteButtonContainer>
|
||||
)}
|
||||
</StyledNodeContainer>
|
||||
|
||||
{!data.hasNextStepIds && (
|
||||
<StyledAddStepButtonContainer
|
||||
shouldDisplay={
|
||||
isHovered ||
|
||||
selected ||
|
||||
isNodeCreationStarted({ parentStepId: data.stepId })
|
||||
}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<WorkflowDiagramCreateStepElement data={data} />
|
||||
</StyledAddStepButtonContainer>
|
||||
)}
|
||||
|
||||
<WorkflowDiagramHandleEditable
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
selected={handlesSelectedState.sourceHandle || selected}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+61
@@ -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 (
|
||||
<Icon size={theme.icon.size.lg} color={theme.font.color.tertiary} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return assertUnreachable(data.triggerType);
|
||||
}
|
||||
case 'action': {
|
||||
switch (data.actionType) {
|
||||
case 'CODE':
|
||||
case 'HTTP_REQUEST': {
|
||||
return (
|
||||
<Icon
|
||||
size={theme.icon.size.lg}
|
||||
color={theme.color.orange}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'SEND_EMAIL': {
|
||||
return <Icon size={theme.icon.size.lg} color={theme.color.blue} />;
|
||||
}
|
||||
case 'AI_AGENT': {
|
||||
return <Icon size={theme.icon.size.lg} color={theme.color.pink} />;
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<Icon
|
||||
size={theme.icon.size.lg}
|
||||
color={theme.font.color.tertiary}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
+144
@@ -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' && (
|
||||
<WorkflowDiagramHandleReadonly
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
selected={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StyledNodeContainer
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<WorkflowNodeIconContainer>
|
||||
<WorkflowDiagramStepNodeIcon data={data} />
|
||||
</WorkflowNodeIconContainer>
|
||||
|
||||
<WorkflowNodeRightPart>
|
||||
<WorkflowNodeLabelWithCounterPart>
|
||||
<StyledNodeLabel>{capitalize(data.nodeType)}</StyledNodeLabel>
|
||||
</WorkflowNodeLabelWithCounterPart>
|
||||
|
||||
<StyledNodeTitle>{data.name}</StyledNodeTitle>
|
||||
</WorkflowNodeRightPart>
|
||||
</StyledNodeContainer>
|
||||
|
||||
<WorkflowDiagramHandleReadonly
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
selected={selected}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+18
@@ -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 };
|
||||
+14
@@ -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 };
|
||||
+9
@@ -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 };
|
||||
+12
@@ -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 };
|
||||
+13
@@ -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 };
|
||||
+15
@@ -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 };
|
||||
+240
@@ -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' && (
|
||||
<WorkflowDiagramHandleReadonly
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
selected={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StyledNodeContainer
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID}
|
||||
data-status={data.runStatus}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<WorkflowNodeIconContainer>
|
||||
<WorkflowDiagramStepNodeIcon data={data} />
|
||||
</WorkflowNodeIconContainer>
|
||||
|
||||
<WorkflowNodeRightPart>
|
||||
<StyledNodeLabelWithCounterPart>
|
||||
<StyledNodeLabel data-status={data.runStatus}>
|
||||
{capitalize(data.nodeType)}
|
||||
</StyledNodeLabel>
|
||||
|
||||
{data.runStatus === StepStatus.SUCCESS && (
|
||||
<StyledNodeCounter>
|
||||
<StyledColorIcon data-status={StepStatus.SUCCESS}>
|
||||
<IconCheck color={theme.tag.text.turquoise} size={14} />
|
||||
</StyledColorIcon>
|
||||
</StyledNodeCounter>
|
||||
)}
|
||||
|
||||
{data.runStatus === StepStatus.FAILED && (
|
||||
<StyledNodeCounter>
|
||||
<StyledColorIcon data-status={StepStatus.FAILED}>
|
||||
<IconX color={theme.tag.text.red} size={14} />
|
||||
</StyledColorIcon>
|
||||
</StyledNodeCounter>
|
||||
)}
|
||||
|
||||
{(data.runStatus === StepStatus.RUNNING ||
|
||||
data.runStatus === StepStatus.PENDING) && (
|
||||
<StyledNodeCounter>
|
||||
<Loader color="yellow" />
|
||||
</StyledNodeCounter>
|
||||
)}
|
||||
</StyledNodeLabelWithCounterPart>
|
||||
|
||||
<StyledNodeTitle data-status={data.runStatus}>
|
||||
{data.name}
|
||||
</StyledNodeTitle>
|
||||
</WorkflowNodeRightPart>
|
||||
</StyledNodeContainer>
|
||||
|
||||
<WorkflowDiagramHandleReadonly
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
selected={selected}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user