Simplify the component layers for workflow nodes (#10042)

- Remove the `WorkflowDiagramBaseStepNode` component
- Create the `WorkflowDiagramStepNodeIcon` component to centralize the
icon put in workflow step nodes; that was the main task of the
`WorkflowDiagramBaseStepNode` component and the new
`WorkflowDiagramStepNodeIcon` component made it obsolete
- Update the `WorkflowDiagramStepNodeBase` component to be THE low level
component
This commit is contained in:
Baptiste Devessier
2025-02-06 10:42:32 +01:00
committed by GitHub
parent 9838bec004
commit e21cbb2fe2
7 changed files with 305 additions and 312 deletions

View File

@ -1,219 +0,0 @@
import { NODE_BORDER_WIDTH } from '@/workflow/workflow-diagram/constants/NodeBorderWidth';
import { NODE_HANDLE_HEIGHT_PX } from '@/workflow/workflow-diagram/constants/NodeHandleHeightPx';
import { NODE_HANDLE_WIDTH_PX } from '@/workflow/workflow-diagram/constants/NodeHandleWidthPx';
import { NODE_ICON_LEFT_MARGIN } from '@/workflow/workflow-diagram/constants/NodeIconLeftMargin';
import { NODE_ICON_WIDTH } from '@/workflow/workflow-diagram/constants/NodeIconWidth';
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { Handle, Position } from '@xyflow/react';
import React from 'react';
import { capitalize, isDefined } from 'twenty-shared';
import { Label, OverflowingTextWithTooltip } from 'twenty-ui';
const StyledStepNodeContainer = styled.div`
display: flex;
flex-direction: column;
padding-block: ${({ theme }) => theme.spacing(3)};
`;
const StyledStepNodeType = styled.div<{
nodeVariant: WorkflowDiagramNodeVariant;
}>`
${({ nodeVariant, theme }) => {
switch (nodeVariant) {
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 }) => {
switch (variant) {
case 'success': {
return css`
background: ${theme.adaptiveColors.turquoise1};
border-color: ${theme.adaptiveColors.turquoise4};
`;
}
case 'failure': {
return css`
background: ${theme.background.danger};
border-color: ${theme.color.red};
`;
}
default: {
return css`
background: ${theme.adaptiveColors.blue1};
border-color: ${theme.color.blue};
`;
}
}
}}
}
`;
const StyledStepNodeLabel = styled.div<{
variant: WorkflowDiagramNodeVariant;
}>`
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;
.selectable:is(.selected, :focus, :focus-visible) & {
color: ${({ theme }) => theme.font.color.primary};
}
`;
export const StyledHandle = styled(Handle)`
background-color: ${({ theme }) => theme.grayScale.gray25};
border: none;
width: ${NODE_HANDLE_WIDTH_PX}px;
height: ${NODE_HANDLE_HEIGHT_PX}px;
`;
const StyledSourceHandle = styled(StyledHandle)`
background-color: ${({ theme }) => theme.border.color.strong};
left: ${NODE_ICON_WIDTH + NODE_ICON_LEFT_MARGIN + NODE_BORDER_WIDTH}px;
`;
const StyledTargetHandle = styled(StyledSourceHandle)`
left: ${NODE_ICON_WIDTH + NODE_ICON_LEFT_MARGIN + NODE_BORDER_WIDTH}px;
visibility: hidden;
`;
const StyledRightFloatingElementContainer = styled.div`
display: flex;
align-items: center;
position: absolute;
right: ${({ theme }) => theme.spacing(-4)};
bottom: 0;
top: 0;
transform: translateX(100%);
`;
export const WorkflowDiagramBaseStepNode = ({
nodeType,
name,
variant,
Icon,
RightFloatingElement,
}: {
nodeType: WorkflowDiagramStepNodeData['nodeType'];
name: string;
variant: WorkflowDiagramNodeVariant;
Icon?: React.ReactNode;
RightFloatingElement?: React.ReactNode;
}) => {
return (
<StyledStepNodeContainer className="workflow-node-container">
{nodeType !== 'trigger' ? (
<StyledTargetHandle type="target" position={Position.Top} />
) : null}
<StyledStepNodeType variant="small" nodeVariant={variant}>
{capitalize(nodeType)}
</StyledStepNodeType>
<StyledStepNodeInnerContainer variant={variant}>
<StyledStepNodeLabel variant={variant}>
{Icon}
<OverflowingTextWithTooltip text={name} />
</StyledStepNodeLabel>
{isDefined(RightFloatingElement) ? (
<StyledRightFloatingElementContainer>
{RightFloatingElement}
</StyledRightFloatingElementContainer>
) : null}
</StyledStepNodeInnerContainer>
<StyledSourceHandle type="source" position={Position.Bottom} />
</StyledStepNodeContainer>
);
};

View File

@ -1,4 +1,4 @@
import { StyledHandle } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseStepNode';
import { StyledHandle } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase';
import { NODE_BORDER_WIDTH } from '@/workflow/workflow-diagram/constants/NodeBorderWidth';
import { NODE_ICON_LEFT_MARGIN } from '@/workflow/workflow-diagram/constants/NodeIconLeftMargin';
import { NODE_ICON_WIDTH } from '@/workflow/workflow-diagram/constants/NodeIconWidth';

View File

@ -1,4 +1,4 @@
import { WorkflowDiagramBaseStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseStepNode';
import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase';
import styled from '@emotion/styled';
const StyledStepNodeLabelIconContainer = styled.div`
@ -12,7 +12,7 @@ const StyledStepNodeLabelIconContainer = styled.div`
export const WorkflowDiagramEmptyTrigger = () => {
return (
<WorkflowDiagramBaseStepNode
<WorkflowDiagramStepNodeBase
name="Add a Trigger"
nodeType="trigger"
variant="empty"

View File

@ -1,105 +1,219 @@
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { WorkflowDiagramBaseStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseStepNode';
import { NODE_BORDER_WIDTH } from '@/workflow/workflow-diagram/constants/NodeBorderWidth';
import { NODE_HANDLE_HEIGHT_PX } from '@/workflow/workflow-diagram/constants/NodeHandleHeightPx';
import { NODE_HANDLE_WIDTH_PX } from '@/workflow/workflow-diagram/constants/NodeHandleWidthPx';
import { NODE_ICON_LEFT_MARGIN } from '@/workflow/workflow-diagram/constants/NodeIconLeftMargin';
import { NODE_ICON_WIDTH } from '@/workflow/workflow-diagram/constants/NodeIconWidth';
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant';
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
import { useTheme } from '@emotion/react';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { useIcons } from 'twenty-ui';
import { Handle, Position } from '@xyflow/react';
import React from 'react';
import { capitalize, isDefined } from 'twenty-shared';
import { Label, OverflowingTextWithTooltip } from 'twenty-ui';
const StyledStepNodeLabelIconContainer = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.transparent.light};
border-radius: ${({ theme }) => theme.spacing(1)};
const StyledStepNodeContainer = styled.div`
display: flex;
justify-content: center;
padding: ${({ theme }) => theme.spacing(1)};
flex-direction: column;
padding-block: ${({ theme }) => theme.spacing(3)};
`;
const StyledStepNodeType = styled.div<{
nodeVariant: WorkflowDiagramNodeVariant;
}>`
${({ nodeVariant, theme }) => {
switch (nodeVariant) {
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 }) => {
switch (variant) {
case 'success': {
return css`
background: ${theme.adaptiveColors.turquoise1};
border-color: ${theme.adaptiveColors.turquoise4};
`;
}
case 'failure': {
return css`
background: ${theme.background.danger};
border-color: ${theme.color.red};
`;
}
default: {
return css`
background: ${theme.adaptiveColors.blue1};
border-color: ${theme.color.blue};
`;
}
}
}}
}
`;
const StyledStepNodeLabel = styled.div<{
variant: WorkflowDiagramNodeVariant;
}>`
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;
.selectable:is(.selected, :focus, :focus-visible) & {
color: ${({ theme }) => theme.font.color.primary};
}
`;
export const StyledHandle = styled(Handle)`
background-color: ${({ theme }) => theme.grayScale.gray25};
border: none;
width: ${NODE_HANDLE_WIDTH_PX}px;
height: ${NODE_HANDLE_HEIGHT_PX}px;
`;
const StyledSourceHandle = styled(StyledHandle)`
background-color: ${({ theme }) => theme.border.color.strong};
left: ${NODE_ICON_WIDTH + NODE_ICON_LEFT_MARGIN + NODE_BORDER_WIDTH}px;
`;
const StyledTargetHandle = styled(StyledSourceHandle)`
left: ${NODE_ICON_WIDTH + NODE_ICON_LEFT_MARGIN + NODE_BORDER_WIDTH}px;
visibility: hidden;
`;
const StyledRightFloatingElementContainer = styled.div`
display: flex;
align-items: center;
position: absolute;
right: ${({ theme }) => theme.spacing(-4)};
bottom: 0;
top: 0;
transform: translateX(100%);
`;
export const WorkflowDiagramStepNodeBase = ({
data,
nodeType,
name,
variant,
Icon,
RightFloatingElement,
}: {
data: WorkflowDiagramStepNodeData;
nodeType: WorkflowDiagramStepNodeData['nodeType'];
name: string;
variant: WorkflowDiagramNodeVariant;
Icon?: React.ReactNode;
RightFloatingElement?: React.ReactNode;
}) => {
const theme = useTheme();
const { getIcon } = useIcons();
const Icon = getIcon(getWorkflowNodeIconKey(data));
const renderStepIcon = () => {
switch (data.nodeType) {
case 'trigger': {
switch (data.triggerType) {
case 'DATABASE_EVENT': {
return (
<StyledStepNodeLabelIconContainer>
<Icon
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
</StyledStepNodeLabelIconContainer>
);
}
case 'MANUAL': {
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': {
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>
);
}
default: {
return (
<StyledStepNodeLabelIconContainer>
<Icon
size={theme.icon.size.md}
color={theme.font.color.tertiary}
stroke={theme.icon.stroke.sm}
/>
</StyledStepNodeLabelIconContainer>
);
}
}
}
}
};
return (
<WorkflowDiagramBaseStepNode
name={data.name}
variant={variant}
nodeType={data.nodeType}
Icon={renderStepIcon()}
RightFloatingElement={RightFloatingElement}
/>
<StyledStepNodeContainer className="workflow-node-container">
{nodeType !== 'trigger' ? (
<StyledTargetHandle type="target" position={Position.Top} />
) : null}
<StyledStepNodeType variant="small" nodeVariant={variant}>
{capitalize(nodeType)}
</StyledStepNodeType>
<StyledStepNodeInnerContainer variant={variant}>
<StyledStepNodeLabel variant={variant}>
{Icon}
<OverflowingTextWithTooltip text={name} />
</StyledStepNodeLabel>
{isDefined(RightFloatingElement) ? (
<StyledRightFloatingElementContainer>
{RightFloatingElement}
</StyledRightFloatingElementContainer>
) : null}
</StyledStepNodeInnerContainer>
<StyledSourceHandle type="source" position={Position.Bottom} />
</StyledStepNodeContainer>
);
};

View File

@ -1,4 +1,5 @@
import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase';
import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon';
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant';
import { FloatingIconButton, IconTrash } from 'twenty-ui';
@ -16,8 +17,10 @@ export const WorkflowDiagramStepNodeEditableContent = ({
}) => {
return (
<WorkflowDiagramStepNodeBase
data={data}
name={data.name}
variant={variant}
nodeType={data.nodeType}
Icon={<WorkflowDiagramStepNodeIcon data={data} />}
RightFloatingElement={
selected ? (
<FloatingIconButton

View File

@ -0,0 +1,87 @@
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { 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 { useIcons } from 'twenty-ui';
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': {
return (
<StyledStepNodeLabelIconContainer>
<Icon
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
</StyledStepNodeLabelIconContainer>
);
}
case 'MANUAL': {
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': {
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>
);
}
default: {
return (
<StyledStepNodeLabelIconContainer>
<Icon
size={theme.icon.size.md}
color={theme.font.color.tertiary}
stroke={theme.icon.stroke.sm}
/>
</StyledStepNodeLabelIconContainer>
);
}
}
}
}
};

View File

@ -1,4 +1,5 @@
import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase';
import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon';
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
export const WorkflowDiagramStepNodeReadonly = ({
@ -6,5 +7,12 @@ export const WorkflowDiagramStepNodeReadonly = ({
}: {
data: WorkflowDiagramStepNodeData;
}) => {
return <WorkflowDiagramStepNodeBase variant="default" data={data} />;
return (
<WorkflowDiagramStepNodeBase
name={data.name}
variant="default"
nodeType={data.nodeType}
Icon={<WorkflowDiagramStepNodeIcon data={data} />}
/>
);
};