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:
committed by
GitHub
parent
337b6a86ab
commit
bbb0c9a761
@ -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%);
|
||||
|
||||
@ -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;
|
||||
`;
|
||||
|
||||
|
||||
@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
],
|
||||
};
|
||||
@ -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,
|
||||
],
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export const NODE_BORDER_WIDTH = 1;
|
||||
@ -0,0 +1,3 @@
|
||||
import { NODE_HANDLE_WIDTH_PX } from './NodeHandleWidthPx';
|
||||
|
||||
export const NODE_HANDLE_HEIGHT_PX = NODE_HANDLE_WIDTH_PX;
|
||||
@ -0,0 +1 @@
|
||||
export const NODE_HANDLE_WIDTH_PX = 4;
|
||||
@ -0,0 +1,5 @@
|
||||
import { THEME_COMMON } from 'twenty-ui';
|
||||
|
||||
export const NODE_ICON_LEFT_MARGIN = Number(
|
||||
THEME_COMMON.spacing(2).replace('px', ''),
|
||||
);
|
||||
@ -0,0 +1,5 @@
|
||||
import { THEME_COMMON } from 'twenty-ui';
|
||||
|
||||
export const NODE_ICON_WIDTH = Number(
|
||||
THEME_COMMON.spacing(6).replace('px', ''),
|
||||
);
|
||||
@ -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={{
|
||||
|
||||
Reference in New Issue
Block a user