diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBaseStepNode.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBaseStepNode.tsx index 7d5b510a4..f6d461c31 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBaseStepNode.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBaseStepNode.tsx @@ -4,14 +4,14 @@ import { NODE_HANDLE_WIDTH_PX } from '@/workflow/workflow-diagram/constants/Node 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'; -type Variant = 'placeholder'; - const StyledStepNodeContainer = styled.div` display: flex; flex-direction: column; @@ -19,52 +19,109 @@ const StyledStepNodeContainer = styled.div` padding-block: ${({ theme }) => theme.spacing(3)}; `; -const StyledStepNodeType = styled(Label)` - background-color: ${({ theme }) => theme.background.tertiary}; - border-radius: ${({ theme }) => theme.border.radius.sm} - ${({ theme }) => theme.border.radius.sm} 0 0; +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)}; - align-self: flex-start; .selectable.selected &, .selectable:focus &, .selectable:focus-visible & { - background-color: ${({ theme }) => theme.color.blue}; - color: ${({ theme }) => theme.font.color.inverted}; + ${({ nodeVariant, theme }) => { + switch (nodeVariant) { + case 'empty': + case 'default': { + return css` + background-color: ${theme.color.blue}; + color: ${theme.font.color.inverted}; + `; + } + } + }} } -`; +`.withComponent(Label); -const StyledStepNodeInnerContainer = styled.div<{ variant?: Variant }>` +const StyledStepNodeInnerContainer = styled.div<{ + variant: WorkflowDiagramNodeVariant; +}>` background-color: ${({ theme }) => theme.background.secondary}; - border: ${NODE_BORDER_WIDTH}px solid - ${({ theme }) => theme.border.color.medium}; + 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; - box-shadow: ${({ variant, theme }) => - variant === 'placeholder' ? 'none' : theme.boxShadow.strong}; .selectable.selected &, .selectable:focus &, .selectable:focus-visible & { - background-color: ${({ theme }) => theme.accent.quaternary}; - border-color: ${({ theme }) => theme.color.blue}; + ${({ theme, variant }) => { + switch (variant) { + case 'success': { + return css` + background-color: ${theme.adaptiveColors.turquoise1}; + border-color: ${theme.adaptiveColors.turquoise4}; + `; + } + case 'failure': { + return css` + background-color: ${theme.background.danger}; + border-color: ${theme.color.red}; + `; + } + default: { + return css` + background-color: ${theme.accent.quaternary}; + border-color: ${theme.color.blue}; + `; + } + } + }} } `; -const StyledStepNodeLabel = styled.div<{ variant?: Variant }>` +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 }) => - variant === 'placeholder' + variant === 'empty' ? theme.font.color.extraLight : theme.font.color.primary}; max-width: 200px; @@ -106,7 +163,7 @@ export const WorkflowDiagramBaseStepNode = ({ }: { nodeType: WorkflowDiagramStepNodeData['nodeType']; name: string; - variant?: Variant; + variant: WorkflowDiagramNodeVariant; Icon?: React.ReactNode; RightFloatingElement?: React.ReactNode; }) => { @@ -116,7 +173,7 @@ export const WorkflowDiagramBaseStepNode = ({ ) : null} - + {capitalize(nodeType)} diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx index 33e0fd021..60a6bb96c 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx @@ -15,7 +15,7 @@ export const WorkflowDiagramEmptyTrigger = () => { } /> ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx index b9cf73c63..8539c8bd0 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx @@ -1,6 +1,7 @@ import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; import { WorkflowDiagramBaseStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseStepNode'; 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 styled from '@emotion/styled'; @@ -17,9 +18,11 @@ const StyledStepNodeLabelIconContainer = styled.div` export const WorkflowDiagramStepNodeBase = ({ data, + variant, RightFloatingElement, }: { data: WorkflowDiagramStepNodeData; + variant: WorkflowDiagramNodeVariant; RightFloatingElement?: React.ReactNode; }) => { const theme = useTheme(); @@ -93,6 +96,7 @@ export const WorkflowDiagramStepNodeBase = ({ return ( { deleteStep(id); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent.tsx index 183988863..3c98a60a1 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent.tsx @@ -1,19 +1,23 @@ import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase'; import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant'; import { FloatingIconButton, IconTrash } from 'twenty-ui'; export const WorkflowDiagramStepNodeEditableContent = ({ data, selected, + variant, onDelete, }: { data: WorkflowDiagramStepNodeData; + variant: WorkflowDiagramNodeVariant; selected: boolean; onDelete: () => void; }) => { return ( { - return ; + return ; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx index 45e979ea4..31e57d049 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx @@ -2,9 +2,9 @@ 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 { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; import { WorkflowDiagramStepNodeEditableContent } from '../WorkflowDiagramStepNodeEditableContent'; @@ -17,12 +17,43 @@ export default meta; type Story = StoryObj; +const ALL_STEPS = [ + { + 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[]; + export const All: CatalogStory< Story, typeof WorkflowDiagramStepNodeEditableContent > = { args: { onDelete: fn(), + variant: 'default', selected: false, }, parameters: { @@ -37,57 +68,22 @@ export const All: CatalogStory< 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[], + values: ALL_STEPS, props: (data: WorkflowDiagramStepNodeData) => ({ data }), }, ], }, }, - decorators: [ - CatalogDecorator, - (Story) => { - return ( - - - - ); - }, - ], + decorators: [CatalogDecorator, ReactflowDecorator], }; + export const AllSelected: CatalogStory< Story, typeof WorkflowDiagramStepNodeEditableContent > = { args: { onDelete: fn(), + variant: 'default', selected: true, }, parameters: { @@ -103,48 +99,131 @@ export const AllSelected: CatalogStory< 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[], + values: ALL_STEPS, props: (data: WorkflowDiagramStepNodeData) => ({ data }), }, ], }, }, - decorators: [ - CatalogDecorator, - (Story) => { - return ( - - - - ); - }, - ], + 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', + values: ALL_STEPS, + props: (data: WorkflowDiagramStepNodeData) => ({ data }), + }, + ], + }, + }, + decorators: [CatalogDecorator, ReactflowDecorator], +}; + +export const AllSuccessSelected: CatalogStory< + Story, + typeof WorkflowDiagramStepNodeEditableContent +> = { + args: { + 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 }), + }, + ], + }, + }, + decorators: [CatalogDecorator, 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], }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant.ts new file mode 100644 index 000000000..6a7a5ff92 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant.ts @@ -0,0 +1,5 @@ +export type WorkflowDiagramNodeVariant = + | 'default' + | 'success' + | 'failure' + | 'empty'; diff --git a/packages/twenty-front/src/testing/decorators/ReactflowDecorator.tsx b/packages/twenty-front/src/testing/decorators/ReactflowDecorator.tsx new file mode 100644 index 000000000..206604a67 --- /dev/null +++ b/packages/twenty-front/src/testing/decorators/ReactflowDecorator.tsx @@ -0,0 +1,10 @@ +import { Decorator } from '@storybook/react'; +import { ReactFlowProvider } from '@xyflow/react'; + +export const ReactflowDecorator: Decorator = (Story) => { + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/theme/constants/AdaptiveColorsDark.ts b/packages/twenty-ui/src/theme/constants/AdaptiveColorsDark.ts new file mode 100644 index 000000000..c021d1201 --- /dev/null +++ b/packages/twenty-ui/src/theme/constants/AdaptiveColorsDark.ts @@ -0,0 +1,8 @@ +import { THEME_COMMON } from '@ui/theme/constants/ThemeCommon'; + +export const ADAPTIVE_COLORS_DARK = { + turquoise1: THEME_COMMON.color.turquoise80, + turquoise2: THEME_COMMON.color.turquoise70, + turquoise3: THEME_COMMON.color.turquoise60, + turquoise4: THEME_COMMON.color.turquoise50, +}; diff --git a/packages/twenty-ui/src/theme/constants/AdaptiveColorsLight.ts b/packages/twenty-ui/src/theme/constants/AdaptiveColorsLight.ts new file mode 100644 index 000000000..69c9e1593 --- /dev/null +++ b/packages/twenty-ui/src/theme/constants/AdaptiveColorsLight.ts @@ -0,0 +1,8 @@ +import { THEME_COMMON } from '@ui/theme/constants/ThemeCommon'; + +export const ADAPTIVE_COLORS_LIGHT = { + turquoise1: THEME_COMMON.color.turquoise10, + turquoise2: THEME_COMMON.color.turquoise20, + turquoise3: THEME_COMMON.color.turquoise30, + turquoise4: THEME_COMMON.color.turquoise40, +}; diff --git a/packages/twenty-ui/src/theme/constants/ThemeDark.ts b/packages/twenty-ui/src/theme/constants/ThemeDark.ts index 10a1e54cc..049b3ce87 100644 --- a/packages/twenty-ui/src/theme/constants/ThemeDark.ts +++ b/packages/twenty-ui/src/theme/constants/ThemeDark.ts @@ -1,3 +1,4 @@ +import { ADAPTIVE_COLORS_DARK } from '@ui/theme/constants/AdaptiveColorsDark'; import { BLUR_DARK } from '@ui/theme/constants/BlurDark'; import { ILLUSTRATION_ICON_DARK } from '@ui/theme/constants/IllustrationIconDark'; import { SNACK_BAR_DARK, ThemeType } from '..'; @@ -24,5 +25,6 @@ export const THEME_DARK: ThemeType = { tag: TAG_DARK, code: CODE_DARK, IllustrationIcon: ILLUSTRATION_ICON_DARK, + adaptiveColors: ADAPTIVE_COLORS_DARK, }, }; diff --git a/packages/twenty-ui/src/theme/constants/ThemeLight.ts b/packages/twenty-ui/src/theme/constants/ThemeLight.ts index 781bfc635..51c361413 100644 --- a/packages/twenty-ui/src/theme/constants/ThemeLight.ts +++ b/packages/twenty-ui/src/theme/constants/ThemeLight.ts @@ -1,3 +1,4 @@ +import { ADAPTIVE_COLORS_LIGHT } from '@ui/theme/constants/AdaptiveColorsLight'; import { BLUR_LIGHT } from '@ui/theme/constants/BlurLight'; import { ILLUSTRATION_ICON_LIGHT } from '@ui/theme/constants/IllustrationIconLight'; import { SNACK_BAR_LIGHT } from '@ui/theme/constants/SnackBarLight'; @@ -24,5 +25,6 @@ export const THEME_LIGHT = { tag: TAG_LIGHT, code: CODE_LIGHT, IllustrationIcon: ILLUSTRATION_ICON_LIGHT, + adaptiveColors: ADAPTIVE_COLORS_LIGHT, }, }; diff --git a/packages/twenty-ui/src/theme/index.ts b/packages/twenty-ui/src/theme/index.ts index a1e090271..0aa0af364 100644 --- a/packages/twenty-ui/src/theme/index.ts +++ b/packages/twenty-ui/src/theme/index.ts @@ -1,5 +1,7 @@ export * from './constants/AccentDark'; export * from './constants/AccentLight'; +export * from './constants/AdaptiveColorsDark'; +export * from './constants/AdaptiveColorsLight'; export * from './constants/Animation'; export * from './constants/BackgroundDark'; export * from './constants/BackgroundLight';