Improve the design of workflow nodes (#9810)

- Go over every node in the workflows and fix the styles to conform to
Figma
- Create stories for every node type
This commit is contained in:
Baptiste Devessier
2025-01-23 16:12:37 +01:00
committed by GitHub
parent 337b6a86ab
commit bbb0c9a761
15 changed files with 329 additions and 37 deletions

View File

@ -1,3 +1,8 @@
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 styled from '@emotion/styled';
import { Handle, Position } from '@xyflow/react';
@ -21,7 +26,7 @@ const StyledStepNodeType = styled.div`
${({ theme }) => theme.border.radius.sm} 0 0;
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.md};
font-size: 9px;
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-left: ${({ theme }) => theme.spacing(2)};
@ -38,9 +43,8 @@ const StyledStepNodeType = styled.div`
const StyledStepNodeInnerContainer = styled.div<{ variant?: Variant }>`
background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-style: ${({ variant }) =>
variant === 'placeholder' ? 'dashed' : null};
border: ${NODE_BORDER_WIDTH}px solid
${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
@ -61,7 +65,7 @@ const StyledStepNodeInnerContainer = styled.div<{ variant?: Variant }>`
const StyledStepNodeLabel = styled.div<{ variant?: Variant }>`
align-items: center;
display: flex;
font-size: ${({ theme }) => theme.font.size.lg};
font-size: 13px;
font-weight: ${({ theme }) => theme.font.weight.medium};
column-gap: ${({ theme }) => theme.spacing(2)};
color: ${({ variant, theme }) =>
@ -71,16 +75,19 @@ const StyledStepNodeLabel = styled.div<{ variant?: Variant }>`
max-width: 200px;
`;
const StyledSourceHandle = styled(Handle)`
export const StyledHandle = styled(Handle)`
background-color: ${({ theme }) => theme.grayScale.gray25};
border: none;
width: 4px;
height: 4px;
left: ${({ theme }) => theme.spacing(10)};
width: ${NODE_HANDLE_WIDTH_PX}px;
height: ${NODE_HANDLE_HEIGHT_PX}px;
`;
export const StyledTargetHandle = styled(Handle)`
left: ${({ theme }) => theme.spacing(10)};
const StyledSourceHandle = styled(StyledHandle)`
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;
`;
@ -88,7 +95,7 @@ const StyledRightFloatingElementContainer = styled.div`
display: flex;
align-items: center;
position: absolute;
right: ${({ theme }) => theme.spacing(-3)};
right: ${({ theme }) => theme.spacing(-4)};
bottom: 0;
top: 0;
transform: translateX(100%);

View File

@ -1,15 +1,19 @@
import { StyledHandle } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseStepNode';
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';
import styled from '@emotion/styled';
import { Handle, Position } from '@xyflow/react';
import { Position } from '@xyflow/react';
import { IconButton, IconPlus } from 'twenty-ui';
const StyledContainer = styled.div`
padding-left: ${({ theme }) => theme.spacing(6)};
padding-top: ${({ theme }) => theme.spacing(1)};
transform: translateX(-50%);
position: relative;
left: ${NODE_ICON_WIDTH + NODE_ICON_LEFT_MARGIN + NODE_BORDER_WIDTH}px;
`;
export const StyledTargetHandle = styled(Handle)`
left: ${({ theme }) => theme.spacing(10)};
const StyledTargetHandle = styled(StyledHandle)`
visibility: hidden;
`;

View File

@ -22,7 +22,10 @@ export const WorkflowDiagramEmptyTrigger = () => {
variant="placeholder"
Icon={
<StyledStepNodeLabelIconContainer>
<IconPlaylistAdd size={16} color={theme.font.color.tertiary} />
<IconPlaylistAdd
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
</StyledStepNodeLabelIconContainer>
}
/>

View File

@ -33,7 +33,7 @@ export const WorkflowDiagramStepNodeBase = ({
return (
<StyledStepNodeLabelIconContainer>
<Icon
size={theme.icon.size.lg}
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
</StyledStepNodeLabelIconContainer>
@ -43,7 +43,7 @@ export const WorkflowDiagramStepNodeBase = ({
return (
<StyledStepNodeLabelIconContainer>
<Icon
size={theme.icon.size.lg}
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
</StyledStepNodeLabelIconContainer>
@ -58,14 +58,18 @@ export const WorkflowDiagramStepNodeBase = ({
case 'CODE': {
return (
<StyledStepNodeLabelIconContainer>
<Icon size={theme.icon.size.lg} color={theme.color.orange} />
<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.lg} color={theme.color.blue} />
<Icon size={theme.icon.size.md} color={theme.color.blue} />
</StyledStepNodeLabelIconContainer>
);
}
@ -75,7 +79,7 @@ export const WorkflowDiagramStepNodeBase = ({
return (
<StyledStepNodeLabelIconContainer>
<Icon
size={theme.icon.size.lg}
size={theme.icon.size.md}
color={theme.font.color.tertiary}
stroke={theme.icon.stroke.sm}
/>

View File

@ -1,11 +1,10 @@
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined';
import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase';
import { WorkflowDiagramStepNodeEditableContent } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent';
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
import { useRecoilValue } from 'recoil';
import { FloatingIconButton, IconTrash } from 'twenty-ui';
export const WorkflowDiagramStepNodeEditable = ({
id,
@ -26,19 +25,12 @@ export const WorkflowDiagramStepNodeEditable = ({
});
return (
<WorkflowDiagramStepNodeBase
<WorkflowDiagramStepNodeEditableContent
data={data}
RightFloatingElement={
selected ? (
<FloatingIconButton
size="medium"
Icon={IconTrash}
onClick={() => {
deleteStep(id);
}}
/>
) : undefined
}
selected={selected ?? false}
onDelete={() => {
deleteStep(id);
}}
/>
);
};

View File

@ -0,0 +1,28 @@
import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase';
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { FloatingIconButton, IconTrash } from 'twenty-ui';
export const WorkflowDiagramStepNodeEditableContent = ({
data,
selected,
onDelete,
}: {
data: WorkflowDiagramStepNodeData;
selected: boolean;
onDelete: () => void;
}) => {
return (
<WorkflowDiagramStepNodeBase
data={data}
RightFloatingElement={
selected ? (
<FloatingIconButton
size="medium"
Icon={IconTrash}
onClick={onDelete}
/>
) : undefined
}
/>
);
};

View File

@ -0,0 +1,42 @@
import { Meta, StoryObj } from '@storybook/react';
import { ReactFlowProvider } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { ComponentDecorator } from 'twenty-ui';
import { WorkflowDiagramCreateStepNode } from '../WorkflowDiagramCreateStepNode';
const meta: Meta<typeof WorkflowDiagramCreateStepNode> = {
title: 'Modules/Workflow/WorkflowDiagramCreateStepNode',
component: WorkflowDiagramCreateStepNode,
decorators: [
ComponentDecorator,
(Story) => (
<ReactFlowProvider>
<Story />
</ReactFlowProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof WorkflowDiagramCreateStepNode>;
export const Default: Story = {
decorators: [
(Story) => (
<div style={{ position: 'relative' }}>
<Story />
</div>
),
],
};
export const Selected: Story = {
decorators: [
(Story) => (
<div className="selectable selected" style={{ position: 'relative' }}>
<Story />
</div>
),
],
};

View File

@ -0,0 +1,43 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { ReactFlowProvider } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { WorkflowDiagramEmptyTrigger } from '../WorkflowDiagramEmptyTrigger';
const meta: Meta<typeof WorkflowDiagramEmptyTrigger> = {
title: 'Modules/Workflow/WorkflowDiagramEmptyTrigger',
component: WorkflowDiagramEmptyTrigger,
};
export default meta;
type Story = StoryObj<typeof WorkflowDiagramEmptyTrigger>;
export const Default: Story = {
decorators: [
(Story) => (
<ReactFlowProvider>
<div style={{ position: 'relative' }}>
<Story />
</div>
</ReactFlowProvider>
),
ComponentDecorator,
],
};
export const Selected: Story = {
decorators: [
(Story) => (
<div className="selectable selected" style={{ position: 'relative' }}>
<Story />
</div>
),
(Story) => (
<ReactFlowProvider>
<Story />
</ReactFlowProvider>
),
ComponentDecorator,
],
};

View File

@ -0,0 +1,150 @@
import { Meta, StoryObj } from '@storybook/react';
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { fn } from '@storybook/test';
import { ReactFlowProvider } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { CatalogDecorator, CatalogStory } from 'twenty-ui';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { WorkflowDiagramStepNodeEditableContent } from '../WorkflowDiagramStepNodeEditableContent';
const meta: Meta<typeof WorkflowDiagramStepNodeEditableContent> = {
title: 'Modules/Workflow/WorkflowDiagramStepNodeEditableContent',
component: WorkflowDiagramStepNodeEditableContent,
};
export default meta;
type Story = StoryObj<typeof WorkflowDiagramStepNodeEditableContent>;
export const All: CatalogStory<
Story,
typeof WorkflowDiagramStepNodeEditableContent
> = {
args: {
onDelete: fn(),
selected: false,
},
parameters: {
msw: graphqlMocks,
catalog: {
options: {
elementContainer: {
width: 250,
style: { position: 'relative' },
},
},
dimensions: [
{
name: 'step type',
values: [
{
nodeType: 'trigger',
triggerType: 'DATABASE_EVENT',
name: 'Record is Created',
},
{ nodeType: 'trigger', triggerType: 'MANUAL', name: 'Manual' },
{
nodeType: 'action',
actionType: 'CREATE_RECORD',
name: 'Create Record',
},
{
nodeType: 'action',
actionType: 'UPDATE_RECORD',
name: 'Update Record',
},
{
nodeType: 'action',
actionType: 'DELETE_RECORD',
name: 'Delete Record',
},
{
nodeType: 'action',
actionType: 'SEND_EMAIL',
name: 'Send Email',
},
{ nodeType: 'action', actionType: 'CODE', name: 'Code' },
] satisfies WorkflowDiagramStepNodeData[],
props: (data: WorkflowDiagramStepNodeData) => ({ data }),
},
],
},
},
decorators: [
CatalogDecorator,
(Story) => {
return (
<ReactFlowProvider>
<Story />
</ReactFlowProvider>
);
},
],
};
export const AllSelected: CatalogStory<
Story,
typeof WorkflowDiagramStepNodeEditableContent
> = {
args: {
onDelete: fn(),
selected: true,
},
parameters: {
msw: graphqlMocks,
catalog: {
options: {
elementContainer: {
width: 250,
style: { position: 'relative' },
className: 'selectable selected',
},
},
dimensions: [
{
name: 'step type',
values: [
{
nodeType: 'trigger',
triggerType: 'DATABASE_EVENT',
name: 'Record is Created',
},
{ nodeType: 'trigger', triggerType: 'MANUAL', name: 'Manual' },
{
nodeType: 'action',
actionType: 'CREATE_RECORD',
name: 'Create Record',
},
{
nodeType: 'action',
actionType: 'UPDATE_RECORD',
name: 'Update Record',
},
{
nodeType: 'action',
actionType: 'DELETE_RECORD',
name: 'Delete Record',
},
{
nodeType: 'action',
actionType: 'SEND_EMAIL',
name: 'Send Email',
},
{ nodeType: 'action', actionType: 'CODE', name: 'Code' },
] satisfies WorkflowDiagramStepNodeData[],
props: (data: WorkflowDiagramStepNodeData) => ({ data }),
},
],
},
},
decorators: [
CatalogDecorator,
(Story) => {
return (
<ReactFlowProvider>
<Story />
</ReactFlowProvider>
);
},
],
};

View File

@ -0,0 +1 @@
export const NODE_BORDER_WIDTH = 1;

View File

@ -0,0 +1,3 @@
import { NODE_HANDLE_WIDTH_PX } from './NodeHandleWidthPx';
export const NODE_HANDLE_HEIGHT_PX = NODE_HANDLE_WIDTH_PX;

View File

@ -0,0 +1 @@
export const NODE_HANDLE_WIDTH_PX = 4;

View File

@ -0,0 +1,5 @@
import { THEME_COMMON } from 'twenty-ui';
export const NODE_ICON_LEFT_MARGIN = Number(
THEME_COMMON.spacing(2).replace('px', ''),
);

View File

@ -0,0 +1,5 @@
import { THEME_COMMON } from 'twenty-ui';
export const NODE_ICON_WIDTH = Number(
THEME_COMMON.spacing(6).replace('px', ''),
);

View File

@ -1,7 +1,7 @@
import { ComponentProps, JSX } from 'react';
import styled from '@emotion/styled';
import { isNumber, isString } from '@sniptt/guards';
import { Decorator } from '@storybook/react';
import { ComponentProps, JSX } from 'react';
const StyledColumnTitle = styled.h1`
font-size: ${({ theme }) => theme.font.size.lg};
@ -91,6 +91,8 @@ export type CatalogDimension<
export type CatalogOptions = {
elementContainer?: {
width?: number;
style?: React.CSSProperties;
className?: string;
};
};
@ -135,6 +137,8 @@ export const CatalogDecorator: Decorator = (Story, context) => {
</StyledElementTitle>
<StyledElementContainer
width={options?.elementContainer?.width}
style={options?.elementContainer?.style}
className={options?.elementContainer?.className}
>
<Story
args={{