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:
committed by
GitHub
parent
ce1ffa8550
commit
a05a9c8f79
-72
@@ -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>
|
||||
);
|
||||
};
|
||||
-31
@@ -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();
|
||||
},
|
||||
};
|
||||
+45
-23
@@ -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}
|
||||
|
||||
+1
-3
@@ -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();
|
||||
|
||||
|
||||
+3
-2
@@ -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.`}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<StyledCalloutContainer variant={variant}>
|
||||
<StyledHeader>
|
||||
<StyledIconContainer variant={variant}>
|
||||
<IconHelp size={16} />
|
||||
<Icon size={16} />
|
||||
</StyledIconContainer>
|
||||
<StyledTitle>{title}</StyledTitle>
|
||||
<IconButton
|
||||
Icon={IconX}
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
ariaLabel="Close"
|
||||
onClick={onClose}
|
||||
/>
|
||||
{isClosable && (
|
||||
<LightIconButton
|
||||
Icon={IconX}
|
||||
size="small"
|
||||
aria-label="Close"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
)}
|
||||
</StyledHeader>
|
||||
<StyledDescriptionWrapper>
|
||||
<StyledDescriptionWrapper hasAction={isDefined(action)}>
|
||||
<StyledDescription>{description}</StyledDescription>
|
||||
</StyledDescriptionWrapper>
|
||||
{isDefined(action) && (
|
||||
|
||||
@@ -19,10 +19,11 @@ type Story = StoryObj<typeof Callout>;
|
||||
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<Story, typeof Callout> = {
|
||||
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<Story, typeof Callout> = {
|
||||
{
|
||||
name: 'accents',
|
||||
values: [
|
||||
'warning',
|
||||
'neutral',
|
||||
'error',
|
||||
'info',
|
||||
'success',
|
||||
'warning',
|
||||
'error',
|
||||
'neutral',
|
||||
'info',
|
||||
] satisfies CalloutVariant[],
|
||||
props: (variant: CalloutVariant) => ({ variant }),
|
||||
},
|
||||
],
|
||||
options: {
|
||||
elementContainer: {
|
||||
width: 512,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [CatalogDecorator],
|
||||
|
||||
Reference in New Issue
Block a user