From a05a9c8f796af159ca39767c090fc99c68f0905c Mon Sep 17 00:00:00 2001 From: Thomas des Francs Date: Wed, 18 Feb 2026 20:23:38 +0100 Subject: [PATCH] refactor workflow action messaging with callout (#18038) ## Summary - remove the dedicated `WorkflowMessage` component and story - update workflow action editor components to use the shared callout patterns - adjust `Callout` and its stories to support the new usage in workflow actions ## Before/After image image --- .../components/WorkflowMessage.tsx | 72 ------------------ .../__stories__/WorkflowMessage.stories.tsx | 31 -------- .../WorkflowEditActionFormBuilder.tsx | 68 +++++++++++------ .../WorkflowEditActionFormBuilder.stories.tsx | 4 +- .../WorkflowEditActionLogicFunction.tsx | 5 +- .../twenty-ui/src/display/callout/Callout.tsx | 76 +++++++++++++------ .../callout/__stories__/Callout.stories.tsx | 27 ++++--- 7 files changed, 120 insertions(+), 163 deletions(-) delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowMessage.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowMessage.stories.tsx diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowMessage.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowMessage.tsx deleted file mode 100644 index 56fbde23b0..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowMessage.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer'; -import { FormFieldInputInnerContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputInnerContainer'; -import { FormFieldInputRowContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputRowContainer'; -import styled from '@emotion/styled'; - -const StyledMessageContainer = styled.div` - padding-bottom: ${({ theme }) => theme.spacing(4)}; - padding-inline: ${({ theme }) => theme.spacing(7)}; - padding-top: ${({ theme }) => theme.spacing(2)}; -`; - -const StyledMessageContentContainer = styled.div` - flex-direction: column; - color: ${({ theme }) => theme.font.color.secondary}; - display: flex; - gap: ${({ theme }) => theme.spacing(4)}; - width: 100%; - padding: ${({ theme }) => theme.spacing(4)}; - line-height: normal; -`; - -const StyledMessageTitle = styled.div` - color: ${({ theme }) => theme.font.color.primary}; - font-weight: ${({ theme }) => theme.font.weight.medium}; - line-height: 13px; -`; - -const StyledMessageDescription = styled.div` - color: ${({ theme }) => theme.font.color.secondary}; - font-weight: ${({ theme }) => theme.font.weight.regular}; -`; - -const StyledFieldContainer = styled.div` - align-items: center; - background: transparent; - border: none; - display: flex; - font-family: inherit; - width: 100%; -`; - -export const WorkflowMessage = ({ - title, - description, -}: { - title: string; - description: string; -}) => { - return ( - - - - - - - - {title} - - - {description} - - - - - - - - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowMessage.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowMessage.stories.tsx deleted file mode 100644 index 8d2e5c2404..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowMessage.stories.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { WorkflowMessage } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowMessage'; -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { expect, within } from 'storybook/test'; - -const meta: Meta = { - title: 'Modules/Workflow/Actions/Form/WorkflowMessage', - component: WorkflowMessage, - parameters: { - layout: 'centered', - }, - decorators: [], -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: {}, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const messageTitleContainer = await canvas.findByTestId( - 'workflow-message-title', - ); - const messageDescriptionContainer = await canvas.findByTestId( - 'workflow-message-description', - ); - - expect(messageTitleContainer).toBeVisible(); - expect(messageDescriptionContainer).toBeVisible(); - }, -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx index c03215288d..f690785456 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx @@ -11,7 +11,6 @@ import { } from '@/workflow/types/Workflow'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter'; -import { WorkflowMessage } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowMessage'; import { WorkflowEditActionFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFieldSettings'; import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField'; import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings'; @@ -25,6 +24,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { Callout, + IconAlertTriangle, IconChevronDown, IconGripVertical, IconPlus, @@ -51,7 +51,8 @@ type FormData = WorkflowFormActionField[]; const StyledWorkflowStepBody = styled(WorkflowStepBody)` display: block; - padding-inline: ${({ theme }) => theme.spacing(2)}; + padding-left: ${({ theme }) => theme.spacing(2)}; + padding-right: ${({ theme }) => theme.spacing(2)}; `; const StyledFormFieldContainer = styled.div` @@ -98,7 +99,8 @@ const StyledFieldContainer = styled.div<{ border: none; display: flex; font-family: inherit; - padding-inline: ${({ theme }) => theme.spacing(2)}; + padding-left: ${({ theme }) => theme.spacing(2)}; + padding-right: ${({ theme }) => theme.spacing(2)}; width: 100%; cursor: ${({ readonly }) => (readonly ? 'default' : 'pointer')}; @@ -118,7 +120,8 @@ const StyledPlaceholder = styled(FormFieldPlaceholder)` `; const StyledAddFieldButtonContainer = styled.div` - padding-inline: ${({ theme }) => theme.spacing(7)}; + padding-left: ${({ theme }) => theme.spacing(7)}; + padding-right: ${({ theme }) => theme.spacing(7)}; padding-top: ${({ theme }) => theme.spacing(2)}; `; @@ -132,6 +135,17 @@ const StyledAddFieldButtonContentContainer = styled.div` width: 100%; `; +const StyledCalloutContainer = styled.div` + padding-bottom: ${({ theme }) => theme.spacing(2)}; + padding-left: ${({ theme }) => theme.spacing(7)}; + padding-right: ${({ theme }) => theme.spacing(7)}; + padding-top: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledNotClosableCalloutContainer = styled(StyledCalloutContainer)` + padding-bottom: ${({ theme }) => theme.spacing(4)}; +`; + export const WorkflowEditActionFormBuilder = ({ triggerType, action, @@ -221,27 +235,35 @@ export const WorkflowEditActionFormBuilder = ({ <> {triggerType && triggerType !== 'MANUAL' && isCalloutVisible && ( - setIsCalloutVisible(false)} - action={{ - label: t`Learn more`, - onClick: () => - window.open( - 'https://docs.twenty.com/user-guide/workflows/capabilities/workflow-actions#form', - '_blank', - 'noopener,noreferrer', - ), - }} - /> + + setIsCalloutVisible(false)} + action={{ + label: t`Learn more`, + onClick: () => + window.open( + 'https://docs.twenty.com/user-guide/workflows/capabilities/workflow-actions#form', + '_blank', + 'noopener,noreferrer', + ), + }} + /> + )} {formData.length === 0 && ( - + + + )} { const canvas = within(canvasElement); - const messageContainer = await canvas.findByTestId( - 'workflow-message-title', - ); + const messageContainer = await canvas.findByText('Add inputs to your form'); expect(messageContainer).toBeVisible(); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/logic-function-action/components/WorkflowEditActionLogicFunction.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/logic-function-action/components/WorkflowEditActionLogicFunction.tsx index 17c2183b83..5a2a268903 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/logic-function-action/components/WorkflowEditActionLogicFunction.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/logic-function-action/components/WorkflowEditActionLogicFunction.tsx @@ -6,13 +6,13 @@ import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowS import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter'; import { WorkflowEditActionCodeFields } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields'; import { setNestedValue } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/setNestedValue'; -import { WorkflowMessage } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowMessage'; import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; import { isObject } from '@sniptt/guards'; import { useMemo } from 'react'; import { isDefined } from 'twenty-shared/utils'; +import { Callout } from 'twenty-ui/display'; import { useDebouncedCallback } from 'use-debounce'; const StyledContainer = styled.div` @@ -118,7 +118,8 @@ export const WorkflowEditActionLogicFunction = ({ fullWidth /> ) : ( - diff --git a/packages/twenty-ui/src/display/callout/Callout.tsx b/packages/twenty-ui/src/display/callout/Callout.tsx index 8083642491..50bac0e9f8 100644 --- a/packages/twenty-ui/src/display/callout/Callout.tsx +++ b/packages/twenty-ui/src/display/callout/Callout.tsx @@ -1,6 +1,8 @@ import styled from '@emotion/styled'; import { IconHelp, IconX } from '@ui/display/icon/components/TablerIcons'; -import { IconButton, LightButton } from '@ui/input'; +import { type IconComponent } from '@ui/display/icon/types/IconComponent'; +import { LightButton, LightIconButton } from '@ui/input'; +import { useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; export type CalloutVariant = @@ -14,22 +16,22 @@ const StyledCalloutContainer = styled.div<{ variant: CalloutVariant }>` align-items: flex-start; background-color: ${({ theme, variant }) => variant === 'info' - ? theme.color.blue1 + ? theme.accent.accent1 : variant === 'warning' - ? theme.color.yellow1 + ? theme.color.orange1 : variant === 'success' - ? theme.color.green1 + ? theme.color.turquoise1 : variant === 'error' ? theme.color.red1 : theme.color.gray1}; border: 1px solid ${({ theme, variant }) => variant === 'info' - ? theme.color.blue6 + ? theme.accent.accent6 : variant === 'warning' - ? theme.color.yellow6 + ? theme.color.orange6 : variant === 'success' - ? theme.color.green6 + ? theme.color.turquoise6 : variant === 'error' ? theme.color.red6 : theme.color.gray6}; @@ -38,9 +40,11 @@ const StyledCalloutContainer = styled.div<{ variant: CalloutVariant }>` display: flex; flex-direction: column; gap: ${({ theme }) => theme.spacing(2)}; - margin-bottom: ${({ theme }) => theme.spacing(2)}; + max-width: 512px; + overflow: hidden; padding: ${({ theme }) => `${theme.spacing(3)} ${theme.spacing(3)} ${theme.spacing(2)}`}; + width: 100%; `; const StyledHeader = styled.div` @@ -49,6 +53,7 @@ const StyledHeader = styled.div` display: flex; flex-direction: row; gap: ${({ theme }) => theme.spacing(2)}; + min-height: ${({ theme }) => theme.spacing(6)}; `; const StyledIconContainer = styled.div<{ variant: CalloutVariant }>` @@ -56,13 +61,15 @@ const StyledIconContainer = styled.div<{ variant: CalloutVariant }>` align-items: center; justify-content: center; flex-shrink: 0; + height: ${({ theme }) => theme.spacing(4)}; + width: ${({ theme }) => theme.spacing(4)}; color: ${({ theme, variant }) => variant === 'info' - ? theme.color.blue9 + ? theme.accent.accent9 : variant === 'warning' ? theme.color.orange9 : variant === 'success' - ? theme.color.green9 + ? theme.color.turquoise9 : variant === 'error' ? theme.color.red9 : theme.color.gray9}; @@ -75,13 +82,18 @@ const StyledTitle = styled.div` font-size: ${({ theme }) => theme.font.size.md}; font-weight: ${({ theme }) => theme.font.weight.medium}; line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; `; -const StyledDescriptionWrapper = styled.div` +const StyledDescriptionWrapper = styled.div<{ hasAction: boolean }>` + align-items: center; display: flex; - padding-left: ${({ theme }) => theme.spacing(6)}; - padding-bottom: ${({ theme }) => theme.spacing(2)}; align-self: stretch; + padding-bottom: ${({ hasAction, theme }) => + hasAction ? 0 : theme.spacing(2)}; + padding-left: ${({ theme }) => theme.spacing(6)}; `; const StyledDescription = styled.div` @@ -104,10 +116,12 @@ export type CalloutProps = { variant: CalloutVariant; title: string; description: string; + Icon?: IconComponent; action?: { label: string; onClick: () => void; }; + isClosable?: boolean; onClose?: () => void; }; @@ -115,25 +129,43 @@ export const Callout = ({ variant, title, description, + Icon = IconHelp, action, + isClosable = false, onClose, }: CalloutProps) => { + const [isVisible, setIsVisible] = useState(true); + + const handleClose = () => { + if (!isClosable) { + return; + } + + setIsVisible(false); + onClose?.(); + }; + + if (!isVisible) { + return null; + } + return ( - + {title} - + {isClosable && ( + + )} - + {description} {isDefined(action) && ( diff --git a/packages/twenty-ui/src/display/callout/__stories__/Callout.stories.tsx b/packages/twenty-ui/src/display/callout/__stories__/Callout.stories.tsx index 3e10d43b7e..cfb3f2f093 100644 --- a/packages/twenty-ui/src/display/callout/__stories__/Callout.stories.tsx +++ b/packages/twenty-ui/src/display/callout/__stories__/Callout.stories.tsx @@ -19,10 +19,11 @@ type Story = StoryObj; export const Default: Story = { args: { variant: 'neutral', - title: 'An callout component', - description: 'Description of callout component', + title: 'This form will appear in workflow runs.', + description: + 'Because this workflow is not using a manual trigger, the form will not open on top of the interface. To fill it, open the corresponding workflow run and complete the form there.', action: { - label: 'Learn more link', + label: 'Learn more', onClick: () => {}, }, }, @@ -31,10 +32,11 @@ export const Default: Story = { export const Catalog: CatalogStory = { args: { - title: 'An callout component', - description: 'Description of callout component', + title: 'This form will appear in workflow runs.', + description: + 'Because this workflow is not using a manual trigger, the form will not open on top of the interface. To fill it, open the corresponding workflow run and complete the form there.', action: { - label: 'Learn more link', + label: 'Learn more', onClick: () => {}, }, }, @@ -47,15 +49,20 @@ export const Catalog: CatalogStory = { { name: 'accents', values: [ - 'warning', - 'neutral', - 'error', - 'info', 'success', + 'warning', + 'error', + 'neutral', + 'info', ] satisfies CalloutVariant[], props: (variant: CalloutVariant) => ({ variant }), }, ], + options: { + elementContainer: { + width: 512, + }, + }, }, }, decorators: [CatalogDecorator],