Only display Flow for Workflow Runs and display Output tab for triggers (#11520)

> [!WARNING]
> I refactored a bunch of components into utility functions to make it
possible to display the `WorkflowStepHeader` component for **triggers**
in the `CommandMenuWorkflowRunViewStep` component. Previously, we were
asserting that we were displaying the header in `Output` and `Input`
tabs only for **actions**. Handling triggers too required a bunch of
changes. We can think of making a bigger refactor of this part.

In this PR:

- Only display the Flow for Workflow Runs; removed the Code Editor tab
- Allows users to see the Output of trigger nodes
- Prevent impossible states by manually setting the selected tab when
selecting a node

## Demo

### Success, Running and Not Executed steps


https://github.com/user-attachments/assets/c6bebd0f-5da2-4ccc-aef2-d9890eafa59a

### Failed step


https://github.com/user-attachments/assets/e1f4e13a-2f5e-4792-a089-928e4d6b1ac0

Closes https://github.com/twentyhq/core-team-issues/issues/709
This commit is contained in:
Baptiste Devessier
2025-04-11 14:31:34 +02:00
committed by GitHub
parent c8011da4d7
commit e8488e1da0
25 changed files with 268 additions and 234 deletions

View File

@ -6,6 +6,7 @@ import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowS
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { CRON_TRIGGER_INTERVAL_OPTIONS } from '@/workflow/workflow-trigger/constants/CronTriggerIntervalOptions';
import { getCronTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getCronTriggerDefaultSettings';
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
import { useTheme } from '@emotion/react';
@ -48,18 +49,11 @@ export const WorkflowEditTriggerCronForm = ({
const { getIcon } = useIcons();
const headerIcon = getTriggerIcon({
type: 'CRON',
});
const headerIcon = getTriggerIcon(trigger);
const defaultLabel =
getTriggerDefaultLabel({
type: 'CRON',
}) ?? '';
const headerTitle = isDefined(trigger.name) ? trigger.name : defaultLabel;
const headerType = 'Trigger';
const defaultLabel = getTriggerDefaultLabel(trigger);
const headerTitle = trigger.name ?? defaultLabel;
const headerType = getTriggerHeaderType(trigger);
const onBlur = () => {
setErrorMessagesVisible(true);

View File

@ -12,6 +12,7 @@ import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
import { useTheme } from '@emotion/react';
@ -105,18 +106,9 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
[systemObjects, searchInputValue],
);
const defaultLabel =
getTriggerDefaultLabel({
type: 'DATABASE_EVENT',
eventName: triggerEvent.event,
}) ?? '-';
const headerIcon = getTriggerIcon({
type: 'DATABASE_EVENT',
eventName: triggerEvent.event,
});
const headerType = `Trigger · ${defaultLabel}`;
const defaultLabel = trigger.name ?? getTriggerDefaultLabel(trigger);
const headerIcon = getTriggerIcon(trigger);
const headerType = getTriggerHeaderType(trigger);
const handleOptionClick = (value: string) => {
if (triggerOptions.readonly === true) {

View File

@ -8,11 +8,13 @@ import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowS
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { MANUAL_TRIGGER_AVAILABILITY_OPTIONS } from '@/workflow/workflow-trigger/constants/ManualTriggerAvailabilityOptions';
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
import { useTheme } from '@emotion/react';
import { isDefined } from 'twenty-shared/utils';
import { SelectOption } from 'twenty-ui/input';
import { useIcons } from 'twenty-ui/display';
import { SelectOption } from 'twenty-ui/input';
type WorkflowEditTriggerManualFormProps = {
trigger: WorkflowManualTrigger;
@ -48,11 +50,10 @@ export const WorkflowEditTriggerManualForm = ({
? 'WHEN_RECORD_SELECTED'
: 'EVERYWHERE';
const headerTitle = isDefined(trigger.name) ? trigger.name : 'Manual Trigger';
const headerTitle = trigger.name ?? getTriggerDefaultLabel(trigger);
const headerIcon = getTriggerIcon({
type: 'MANUAL',
});
const headerIcon = getTriggerIcon(trigger);
const headerType = getTriggerHeaderType(trigger);
return (
<>
@ -70,7 +71,7 @@ export const WorkflowEditTriggerManualForm = ({
Icon={getIcon(headerIcon)}
iconColor={theme.font.color.tertiary}
initialTitle={headerTitle}
headerType="Trigger · Manual"
headerType={headerType}
disabled={triggerOptions.readonly}
/>
<WorkflowStepBody>

View File

@ -1,26 +1,28 @@
import { WorkflowWebhookTrigger } from '@/workflow/types/Workflow';
import { useTheme } from '@emotion/react';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useDebouncedCallback } from 'use-debounce';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { isDefined } from 'twenty-shared/utils';
import { useIcons, IconCopy } from 'twenty-ui/display';
import { Select } from '@/ui/input/components/Select';
import { WEBHOOK_TRIGGER_HTTP_METHOD_OPTIONS } from '@/workflow/workflow-trigger/constants/WebhookTriggerHttpMethodOptions';
import { getWebhookTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getWebhookTriggerDefaultSettings';
import { WEBHOOK_TRIGGER_AUTHENTICATION_OPTIONS } from '@/workflow/workflow-trigger/constants/WebhookTriggerAuthenticationOptions';
import { FormRawJsonFieldInput } from '@/object-record/record-field/form-types/components/FormRawJsonFieldInput';
import { useState } from 'react';
import { getFunctionOutputSchema } from '@/serverless-functions/utils/getFunctionOutputSchema';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Select } from '@/ui/input/components/Select';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { WorkflowWebhookTrigger } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { WEBHOOK_TRIGGER_AUTHENTICATION_OPTIONS } from '@/workflow/workflow-trigger/constants/WebhookTriggerAuthenticationOptions';
import { WEBHOOK_TRIGGER_HTTP_METHOD_OPTIONS } from '@/workflow/workflow-trigger/constants/WebhookTriggerHttpMethodOptions';
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
import { getWebhookTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getWebhookTriggerDefaultSettings';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconCopy, useIcons } from 'twenty-ui/display';
import { useDebouncedCallback } from 'use-debounce';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
type WorkflowEditTriggerWebhookFormProps = {
trigger: WorkflowWebhookTrigger;
@ -59,11 +61,10 @@ export const WorkflowEditTriggerWebhookForm = ({
setErrorMessagesVisible(true);
};
const headerTitle = isDefined(trigger.name) ? trigger.name : 'Webhook';
const headerTitle = trigger.name ?? getTriggerDefaultLabel(trigger);
const headerIcon = getTriggerIcon({
type: 'WEBHOOK',
});
const headerIcon = getTriggerIcon(trigger);
const headerType = getTriggerHeaderType(trigger);
const webhookUrl = `${REACT_APP_SERVER_BASE_URL}/webhooks/workflows/${currentWorkspace?.id}/${workflowId}`;
const displayWebhookUrl = webhookUrl.replace(/^(https?:\/\/)?(www\.)?/, '');
@ -98,7 +99,7 @@ export const WorkflowEditTriggerWebhookForm = ({
Icon={getIcon(headerIcon)}
iconColor={theme.font.color.tertiary}
initialTitle={headerTitle}
headerType="Trigger · Webhook"
headerType={headerType}
disabled={triggerOptions.readonly}
/>
<WorkflowStepBody>

View File

@ -0,0 +1,25 @@
import { WorkflowTrigger } from '@/workflow/types/Workflow';
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
import { assertUnreachable } from 'twenty-shared/utils';
export const getTriggerHeaderType = (trigger: WorkflowTrigger) => {
switch (trigger.type) {
case 'CRON': {
return 'Trigger';
}
case 'WEBHOOK': {
return 'Trigger · Webhook';
}
case 'MANUAL': {
return 'Trigger · Manual';
}
case 'DATABASE_EVENT': {
const defaultLabel = getTriggerDefaultLabel(trigger);
return `Trigger · ${defaultLabel}`;
}
default: {
assertUnreachable(trigger, 'Unknown trigger type');
}
}
};

View File

@ -1,26 +1,18 @@
import { WorkflowTrigger } from '@/workflow/types/Workflow';
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes';
import { OTHER_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/OtherTriggerTypes';
export const getTriggerIcon = (
trigger:
| {
type: 'MANUAL';
}
| {
type: 'CRON';
}
| {
type: 'WEBHOOK';
}
| {
type: 'DATABASE_EVENT';
eventName: string;
},
trigger: WorkflowTrigger,
): string | undefined => {
if (trigger.type === 'DATABASE_EVENT') {
return DATABASE_TRIGGER_TYPES.find(
(type) => type.event === trigger.eventName,
)?.icon;
const eventName = splitWorkflowTriggerEventName(
trigger.settings.eventName,
).event;
return DATABASE_TRIGGER_TYPES.find((type) => type.event === eventName)
?.icon;
}
return OTHER_TRIGGER_TYPES.find((item) => item.type === trigger.type)?.icon;

View File

@ -0,0 +1,5 @@
import { Theme } from '@emotion/react';
export const getTriggerIconColor = ({ theme }: { theme: Theme }) => {
return theme.font.color.tertiary;
};

View File

@ -1,28 +1,33 @@
import { WorkflowTrigger } from '@/workflow/types/Workflow';
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes';
import { OTHER_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/OtherTriggerTypes';
import { isDefined } from 'twenty-shared/utils';
export const getTriggerDefaultLabel = (
trigger:
| {
type: 'MANUAL';
}
| {
type: 'CRON';
}
| {
type: 'WEBHOOK';
}
| {
type: 'DATABASE_EVENT';
eventName: string;
},
): string | undefined => {
export const getTriggerDefaultLabel = (trigger: WorkflowTrigger): string => {
if (trigger.type === 'DATABASE_EVENT') {
return DATABASE_TRIGGER_TYPES.find(
(type) => type.event === trigger.eventName,
const triggerEvent = splitWorkflowTriggerEventName(
trigger.settings.eventName,
);
const label = DATABASE_TRIGGER_TYPES.find(
(type) => type.event === triggerEvent.event,
)?.defaultLabel;
if (!isDefined(label)) {
throw new Error('Unknown trigger event');
}
return label;
}
return OTHER_TRIGGER_TYPES.find((item) => item.type === trigger.type)
?.defaultLabel;
const label = OTHER_TRIGGER_TYPES.find(
(item) => item.type === trigger.type,
)?.defaultLabel;
if (!isDefined(label)) {
throw new Error('Unknown trigger type');
}
return label;
};