Add the new workflow not executed node type (#10030)

- Added the new workflow `not executed` node type
- Fixed minor style issues
- Created one big catalog for all node variants


![image](https://github.com/user-attachments/assets/5e510d49-c6a2-42a9-9641-057cff481dd9)
This commit is contained in:
Baptiste Devessier
2025-02-05 18:43:46 +01:00
committed by GitHub
parent 5528577707
commit 700eb2d473
5 changed files with 89 additions and 173 deletions

View File

@ -50,18 +50,16 @@ const StyledStepNodeType = styled.div<{
margin-left: ${({ theme }) => theme.spacing(2)}; margin-left: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
.selectable.selected &, .selectable:is(.selected, :focus, :focus-visible) & {
.selectable:focus &,
.selectable:focus-visible & {
${({ nodeVariant, theme }) => { ${({ nodeVariant, theme }) => {
switch (nodeVariant) { switch (nodeVariant) {
case 'empty': case 'empty':
case 'default': { case 'default':
case 'not-executed':
return css` return css`
background-color: ${theme.color.blue}; background-color: ${theme.color.blue};
color: ${theme.font.color.inverted}; color: ${theme.font.color.inverted};
`; `;
}
} }
}} }}
} }
@ -70,7 +68,7 @@ const StyledStepNodeType = styled.div<{
const StyledStepNodeInnerContainer = styled.div<{ const StyledStepNodeInnerContainer = styled.div<{
variant: WorkflowDiagramNodeVariant; variant: WorkflowDiagramNodeVariant;
}>` }>`
background-color: ${({ theme }) => theme.background.secondary}; background: ${({ theme }) => theme.background.secondary};
border-color: ${({ theme }) => theme.border.color.medium}; border-color: ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md}; border-radius: ${({ theme }) => theme.border.radius.md};
@ -84,26 +82,41 @@ const StyledStepNodeInnerContainer = styled.div<{
position: relative; position: relative;
.selectable.selected &, transition: background ${({ theme }) => theme.animation.duration.fast} ease;
.selectable:focus &,
.selectable:focus-visible & { .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 }) => { ${({ theme, variant }) => {
switch (variant) { switch (variant) {
case 'success': { case 'success': {
return css` return css`
background-color: ${theme.adaptiveColors.turquoise1}; background: ${theme.adaptiveColors.turquoise1};
border-color: ${theme.adaptiveColors.turquoise4}; border-color: ${theme.adaptiveColors.turquoise4};
`; `;
} }
case 'failure': { case 'failure': {
return css` return css`
background-color: ${theme.background.danger}; background: ${theme.background.danger};
border-color: ${theme.color.red}; border-color: ${theme.color.red};
`; `;
} }
default: { default: {
return css` return css`
background-color: ${theme.accent.quaternary}; background: ${theme.adaptiveColors.blue1};
border-color: ${theme.color.blue}; border-color: ${theme.color.blue};
`; `;
} }
@ -120,11 +133,20 @@ const StyledStepNodeLabel = styled.div<{
font-size: 13px; font-size: 13px;
font-weight: ${({ theme }) => theme.font.weight.medium}; font-weight: ${({ theme }) => theme.font.weight.medium};
column-gap: ${({ theme }) => theme.spacing(2)}; column-gap: ${({ theme }) => theme.spacing(2)};
color: ${({ variant, theme }) => color: ${({ variant, theme }) => {
variant === 'empty' switch (variant) {
? theme.font.color.extraLight case 'empty':
: theme.font.color.primary}; case 'not-executed':
return theme.font.color.light;
default:
return theme.font.color.primary;
}
}};
max-width: 200px; max-width: 200px;
.selectable:is(.selected, :focus, :focus-visible) & {
color: ${({ theme }) => theme.font.color.primary};
}
`; `;
export const StyledHandle = styled(Handle)` export const StyledHandle = styled(Handle)`
@ -168,7 +190,7 @@ export const WorkflowDiagramBaseStepNode = ({
RightFloatingElement?: React.ReactNode; RightFloatingElement?: React.ReactNode;
}) => { }) => {
return ( return (
<StyledStepNodeContainer> <StyledStepNodeContainer className="workflow-node-container">
{nodeType !== 'trigger' ? ( {nodeType !== 'trigger' ? (
<StyledTargetHandle type="target" position={Position.Top} /> <StyledTargetHandle type="target" position={Position.Top} />
) : null} ) : null}

View File

@ -1,21 +1,33 @@
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant';
import { fn } from '@storybook/test'; import { fn } from '@storybook/test';
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
import { ComponentProps } from 'react';
import { CatalogDecorator, CatalogStory } from 'twenty-ui'; import { CatalogDecorator, CatalogStory } from 'twenty-ui';
import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator'; import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { WorkflowDiagramStepNodeEditableContent } from '../WorkflowDiagramStepNodeEditableContent'; import { WorkflowDiagramStepNodeEditableContent } from '../WorkflowDiagramStepNodeEditableContent';
const meta: Meta<typeof WorkflowDiagramStepNodeEditableContent> = { type ComponentState = 'default' | 'hover' | 'selected';
type WrapperProps = ComponentProps<
typeof WorkflowDiagramStepNodeEditableContent
> & { state: ComponentState };
const Wrapper = (_props: WrapperProps) => {
return <div></div>;
};
const meta: Meta<WrapperProps> = {
title: 'Modules/Workflow/WorkflowDiagramStepNodeEditableContent', title: 'Modules/Workflow/WorkflowDiagramStepNodeEditableContent',
component: WorkflowDiagramStepNodeEditableContent, component: WorkflowDiagramStepNodeEditableContent,
}; };
export default meta; export default meta;
type Story = StoryObj<typeof WorkflowDiagramStepNodeEditableContent>; type Story = StoryObj<typeof Wrapper>;
const ALL_STEPS = [ const ALL_STEPS = [
{ {
@ -47,17 +59,13 @@ const ALL_STEPS = [
{ nodeType: 'action', actionType: 'CODE', name: 'Code' }, { nodeType: 'action', actionType: 'CODE', name: 'Code' },
] satisfies WorkflowDiagramStepNodeData[]; ] satisfies WorkflowDiagramStepNodeData[];
export const All: CatalogStory< export const Catalog: CatalogStory<Story, typeof Wrapper> = {
Story,
typeof WorkflowDiagramStepNodeEditableContent
> = {
args: { args: {
onDelete: fn(), onDelete: fn(),
variant: 'default',
selected: false,
}, },
parameters: { parameters: {
msw: graphqlMocks, msw: graphqlMocks,
pseudo: { hover: ['.hover'] },
catalog: { catalog: {
options: { options: {
elementContainer: { elementContainer: {
@ -71,159 +79,36 @@ export const All: CatalogStory<
values: ALL_STEPS, values: ALL_STEPS,
props: (data: WorkflowDiagramStepNodeData) => ({ data }), props: (data: WorkflowDiagramStepNodeData) => ({ data }),
}, },
],
},
},
decorators: [CatalogDecorator, ReactflowDecorator],
};
export const AllSelected: CatalogStory<
Story,
typeof WorkflowDiagramStepNodeEditableContent
> = {
args: {
onDelete: fn(),
variant: 'default',
selected: true,
},
parameters: {
msw: graphqlMocks,
catalog: {
options: {
elementContainer: {
width: 250,
style: { position: 'relative' },
className: 'selectable selected',
},
},
dimensions: [
{ {
name: 'step type', name: 'variant',
values: ALL_STEPS, values: [
props: (data: WorkflowDiagramStepNodeData) => ({ data }), 'empty',
'default',
'success',
'failure',
'not-executed',
] satisfies WorkflowDiagramNodeVariant[],
props: (variant: WorkflowDiagramNodeVariant) => ({ variant }),
}, },
],
},
},
decorators: [CatalogDecorator, ReactflowDecorator],
};
export const AllSuccess: CatalogStory<
Story,
typeof WorkflowDiagramStepNodeEditableContent
> = {
args: {
onDelete: fn(),
variant: 'success',
},
parameters: {
msw: graphqlMocks,
catalog: {
options: {
elementContainer: {
width: 250,
style: { position: 'relative' },
},
},
dimensions: [
{ {
name: 'step type', name: 'state',
values: ALL_STEPS, values: ['default', 'hover', 'selected'] satisfies ComponentState[],
props: (data: WorkflowDiagramStepNodeData) => ({ data }), props: (state: ComponentState) => ({ state }),
}, },
], ],
}, },
}, },
decorators: [CatalogDecorator, ReactflowDecorator], decorators: [
}; (Story, { args }) => {
return (
export const AllSuccessSelected: CatalogStory< <div
Story, className={`selectable ${args.state === 'selected' ? 'selected' : args.state === 'hover' ? 'workflow-node-container hover' : ''}`}
typeof WorkflowDiagramStepNodeEditableContent >
> = { <Story />
args: { </div>
onDelete: fn(), );
variant: 'success',
selected: true,
},
parameters: {
msw: graphqlMocks,
catalog: {
options: {
elementContainer: {
width: 250,
style: { position: 'relative' },
className: 'selectable selected',
},
},
dimensions: [
{
name: 'step type',
values: ALL_STEPS,
props: (data: WorkflowDiagramStepNodeData) => ({ data }),
},
],
}, },
}, CatalogDecorator,
decorators: [CatalogDecorator, ReactflowDecorator], ReactflowDecorator,
}; ],
export const AllFailure: CatalogStory<
Story,
typeof WorkflowDiagramStepNodeEditableContent
> = {
args: {
onDelete: fn(),
variant: 'failure',
},
parameters: {
msw: graphqlMocks,
catalog: {
options: {
elementContainer: {
width: 250,
style: { position: 'relative' },
},
},
dimensions: [
{
name: 'step type',
values: ALL_STEPS,
props: (data: WorkflowDiagramStepNodeData) => ({ data }),
},
],
},
},
decorators: [CatalogDecorator, ReactflowDecorator],
};
export const AllFailureSelected: CatalogStory<
Story,
typeof WorkflowDiagramStepNodeEditableContent
> = {
args: {
onDelete: fn(),
variant: 'failure',
selected: true,
},
parameters: {
msw: graphqlMocks,
catalog: {
options: {
elementContainer: {
width: 250,
style: { position: 'relative' },
className: 'selectable selected',
},
},
dimensions: [
{
name: 'step type',
values: ALL_STEPS,
props: (data: WorkflowDiagramStepNodeData) => ({ data }),
},
],
},
},
decorators: [CatalogDecorator, ReactflowDecorator],
}; };

View File

@ -2,4 +2,5 @@ export type WorkflowDiagramNodeVariant =
| 'default' | 'default'
| 'success' | 'success'
| 'failure' | 'failure'
| 'empty'; | 'empty'
| 'not-executed';

View File

@ -5,4 +5,8 @@ export const ADAPTIVE_COLORS_DARK = {
turquoise2: THEME_COMMON.color.turquoise70, turquoise2: THEME_COMMON.color.turquoise70,
turquoise3: THEME_COMMON.color.turquoise60, turquoise3: THEME_COMMON.color.turquoise60,
turquoise4: THEME_COMMON.color.turquoise50, turquoise4: THEME_COMMON.color.turquoise50,
blue1: THEME_COMMON.color.blue80,
blue2: THEME_COMMON.color.blue70,
blue3: THEME_COMMON.color.blue60,
blue4: THEME_COMMON.color.blue50,
}; };

View File

@ -5,4 +5,8 @@ export const ADAPTIVE_COLORS_LIGHT = {
turquoise2: THEME_COMMON.color.turquoise20, turquoise2: THEME_COMMON.color.turquoise20,
turquoise3: THEME_COMMON.color.turquoise30, turquoise3: THEME_COMMON.color.turquoise30,
turquoise4: THEME_COMMON.color.turquoise40, turquoise4: THEME_COMMON.color.turquoise40,
blue1: THEME_COMMON.color.blue10,
blue2: THEME_COMMON.color.blue20,
blue3: THEME_COMMON.color.blue30,
blue4: THEME_COMMON.color.blue40,
}; };