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

<img width="525" height="548" alt="image"
src="https://github.com/user-attachments/assets/ce57a84f-f070-4149-85ef-a4d162b2d878"
/>


<img width="518" height="593" alt="image"
src="https://github.com/user-attachments/assets/f7249cd0-221f-496d-9deb-d9966ee43382"
/>
This commit is contained in:
Thomas des Francs
2026-02-18 20:23:38 +01:00
committed by GitHub
parent ce1ffa8550
commit a05a9c8f79
7 changed files with 120 additions and 163 deletions
@@ -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 (
<StyledMessageContainer>
<FormFieldInputContainer>
<FormFieldInputRowContainer multiline maxHeight={145}>
<FormFieldInputInnerContainer
formFieldInputInstanceId="workflow-message"
hasRightElement={false}
>
<StyledFieldContainer>
<StyledMessageContentContainer>
<StyledMessageTitle data-testid="workflow-message-title">
{title}
</StyledMessageTitle>
<StyledMessageDescription data-testid="workflow-message-description">
{description}
</StyledMessageDescription>
</StyledMessageContentContainer>
</StyledFieldContainer>
</FormFieldInputInnerContainer>
</FormFieldInputRowContainer>
</FormFieldInputContainer>
</StyledMessageContainer>
);
};
@@ -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<typeof WorkflowMessage> = {
title: 'Modules/Workflow/Actions/Form/WorkflowMessage',
component: WorkflowMessage,
parameters: {
layout: 'centered',
},
decorators: [],
};
export default meta;
type Story = StoryObj<typeof WorkflowMessage>;
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();
},
};
@@ -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 = ({
<>
<StyledWorkflowStepBody>
{triggerType && triggerType !== 'MANUAL' && isCalloutVisible && (
<Callout
variant={'warning'}
title={t`This form will appear in workflow runs.`}
description={t`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.`}
onClose={() => setIsCalloutVisible(false)}
action={{
label: t`Learn more`,
onClick: () =>
window.open(
'https://docs.twenty.com/user-guide/workflows/capabilities/workflow-actions#form',
'_blank',
'noopener,noreferrer',
),
}}
/>
<StyledCalloutContainer>
<Callout
variant={'warning'}
Icon={IconAlertTriangle}
title={t`This form will appear in workflow runs.`}
description={t`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.`}
isClosable
onClose={() => setIsCalloutVisible(false)}
action={{
label: t`Learn more`,
onClick: () =>
window.open(
'https://docs.twenty.com/user-guide/workflows/capabilities/workflow-actions#form',
'_blank',
'noopener,noreferrer',
),
}}
/>
</StyledCalloutContainer>
)}
{formData.length === 0 && (
<WorkflowMessage
title={t`Add inputs to your form`}
description={t`Click on "Add Field" below to add the first input to your form. The form will pop up on the user's screen when the workflow is launched from a manual trigger. For other types of triggers, it will be displayed in the Workflow run record page.`}
/>
<StyledNotClosableCalloutContainer>
<Callout
variant={'neutral'}
isClosable={false}
title={t`Add inputs to your form`}
description={t`Click on "Add Field" below to add the first input to your form. The form will pop up on the user's screen when the workflow is launched from a manual trigger. For other types of triggers, it will be displayed in the Workflow run record page.`}
/>
</StyledNotClosableCalloutContainer>
)}
<DraggableList
onDragEnd={handleDragEnd}
@@ -179,9 +179,7 @@ export const EmptyForm: Story = {
},
play: async ({ canvasElement }) => {
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();
@@ -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
/>
) : (
<WorkflowMessage
<Callout
variant={'neutral'}
title={t`No input fields for this action`}
description={t`You can see the function logic in your application settings.`}
/>