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

@ -20,7 +20,6 @@ import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getSh
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
import { WorkflowRunTabId } from '@/workflow/workflow-steps/types/WorkflowRunTabId';
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowServerlessFunctionTabListComponentId';
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowServerlessFunctionTabId';
import { useRecoilCallback } from 'recoil';
@ -66,7 +65,7 @@ export const useCommandMenuCloseAnimationCompleteCleanup = () => {
activeTabIdComponentState.atomFamily({
instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
}),
WorkflowRunTabId.NODE,
null,
);
set(
activeTabIdComponentState.atomFamily({

View File

@ -1,3 +1,5 @@
import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled';
import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled';
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -15,7 +17,6 @@ import {
WorkflowRunTabIdType,
} from '@/workflow/workflow-steps/types/WorkflowRunTabId';
import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui/display';
@ -45,38 +46,39 @@ export const CommandMenuWorkflowRunViewStep = () => {
WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
);
const stepExecutionStatus = isDefined(workflowRun)
? getWorkflowRunStepExecutionStatus({
workflowRunOutput: workflowRun.output,
stepId: workflowSelectedNode,
})
: undefined;
if (!isDefined(workflowRun)) {
return null;
}
const areInputAndOutputTabsDisabled =
workflowSelectedNode === TRIGGER_STEP_ID ||
stepExecutionStatus === 'running' ||
stepExecutionStatus === 'not-executed';
const stepExecutionStatus = getWorkflowRunStepExecutionStatus({
workflowRunOutput: workflowRun.output,
stepId: workflowSelectedNode,
});
const isInputTabDisabled = getIsInputTabDisabled({
stepExecutionStatus,
workflowSelectedNode,
});
const isOutputTabDisabled = getIsOutputTabDisabled({
stepExecutionStatus,
});
const tabs: SingleTabProps<TabId>[] = [
{
id: WorkflowRunTabId.OUTPUT,
title: 'Output',
Icon: IconLogout,
disabled: isOutputTabDisabled,
},
{ id: WorkflowRunTabId.NODE, title: 'Node', Icon: IconStepInto },
{
id: WorkflowRunTabId.INPUT,
title: 'Input',
Icon: IconLogin2,
disabled: areInputAndOutputTabsDisabled,
},
{
id: WorkflowRunTabId.OUTPUT,
title: 'Output',
Icon: IconLogout,
disabled: areInputAndOutputTabsDisabled,
disabled: isInputTabDisabled,
},
];
if (!isDefined(workflowRun)) {
return null;
}
return (
<WorkflowStepContextProvider
value={{
@ -93,6 +95,13 @@ export const CommandMenuWorkflowRunViewStep = () => {
}
/>
{activeTabId === WorkflowRunTabId.OUTPUT ? (
<WorkflowRunStepOutputDetail
key={workflowSelectedNode}
stepId={workflowSelectedNode}
/>
) : null}
{activeTabId === WorkflowRunTabId.NODE ? (
<WorkflowRunStepNodeDetail
stepId={workflowSelectedNode}
@ -108,13 +117,6 @@ export const CommandMenuWorkflowRunViewStep = () => {
stepId={workflowSelectedNode}
/>
) : null}
{activeTabId === WorkflowRunTabId.OUTPUT ? (
<WorkflowRunStepOutputDetail
key={workflowSelectedNode}
stepId={workflowSelectedNode}
/>
) : null}
</StyledContainer>
</WorkflowStepContextProvider>
);

View File

@ -0,0 +1,15 @@
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
export const getIsInputTabDisabled = ({
stepExecutionStatus,
workflowSelectedNode,
}: {
workflowSelectedNode: string;
stepExecutionStatus: WorkflowDiagramRunStatus;
}) => {
return (
workflowSelectedNode === TRIGGER_STEP_ID ||
stepExecutionStatus === 'not-executed'
);
};

View File

@ -0,0 +1,11 @@
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
export const getIsOutputTabDisabled = ({
stepExecutionStatus,
}: {
stepExecutionStatus: WorkflowDiagramRunStatus;
}) => {
return (
stepExecutionStatus === 'running' || stepExecutionStatus === 'not-executed'
);
};

View File

@ -8,7 +8,6 @@ import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableE
import { FieldsCard } from '@/object-record/record-show/components/FieldsCard';
import { CardType } from '@/object-record/record-show/types/CardType';
import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
import { WorkflowRunOutputVisualizer } from '@/workflow/workflow-diagram/components/WorkflowRunOutputVisualizer';
import { WorkflowRunVisualizer } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizer';
import { WorkflowRunVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect';
import { WorkflowVersionVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizer';
@ -104,7 +103,4 @@ export const CardComponents: Record<CardType, CardComponentType> = {
<WorkflowRunVisualizer workflowRunId={targetableObject.id} />
</>
),
[CardType.WorkflowRunOutputCard]: ({ targetableObject }) => (
<WorkflowRunOutputVisualizer workflowRunId={targetableObject.id} />
),
};

View File

@ -17,7 +17,6 @@ import {
IconHome,
IconMail,
IconNotes,
IconPrinter,
IconSettings,
} from 'twenty-ui/display';
@ -189,21 +188,7 @@ export const useRecordShowContainerTabs = (
},
[CoreObjectNameSingular.WorkflowRun]: {
tabs: {
workflowRunOutput: {
title: 'Output',
position: 0,
Icon: IconPrinter,
cards: [{ type: CardType.WorkflowRunOutputCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
workflowRunFlow: {
workflowRun: {
title: 'Flow',
position: 0,
Icon: IconSettings,

View File

@ -9,6 +9,5 @@ export enum CardType {
WorkflowCard = 'WorkflowCard',
WorkflowVersionCard = 'WorkflowVersionCard',
WorkflowRunCard = 'WorkflowRunCard',
WorkflowRunOutputCard = 'WorkflowRunOutputCard',
RichTextCard = 'RichTextCard',
}

View File

@ -1,7 +1,6 @@
import { stepsOutputSchemaFamilyState } from '@/workflow/states/stepsOutputSchemaFamilyState';
import { WorkflowVersion } from '@/workflow/types/Workflow';
import { getStepOutputSchemaFamilyStateKey } from '@/workflow/utils/getStepOutputSchemaFamilyStateKey';
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
@ -36,17 +35,7 @@ export const useStepsOutputSchema = () => {
const trigger = workflowVersion.trigger;
if (isDefined(trigger)) {
const triggerIconKey =
trigger.type === 'DATABASE_EVENT'
? getTriggerIcon({
type: trigger.type,
eventName: splitWorkflowTriggerEventName(
trigger.settings?.eventName,
).event,
})
: getTriggerIcon({
type: trigger.type,
});
const triggerIconKey = getTriggerIcon(trigger);
const triggerOutputSchema: StepOutputSchema = {
id: TRIGGER_STEP_ID,

View File

@ -8,6 +8,7 @@ import {
workflowDatabaseEventTriggerSchema,
workflowDeleteRecordActionSchema,
workflowDeleteRecordActionSettingsSchema,
workflowExecutorOutputSchema,
workflowFindRecordsActionSchema,
workflowFindRecordsActionSettingsSchema,
workflowFormActionSchema,
@ -110,6 +111,9 @@ export type WorkflowVersion = {
};
export type WorkflowRunOutput = z.infer<typeof workflowRunOutputSchema>;
export type WorkflowExecutorOutput = z.infer<
typeof workflowExecutorOutputSchema
>;
export type WorkflowRunOutputStepsOutput = z.infer<
typeof workflowRunOutputStepsOutputSchema
>;

View File

@ -233,7 +233,7 @@ export const workflowTriggerSchema = z.discriminatedUnion('type', [
]);
// Step output schemas
const workflowExecutorOutputSchema = z.object({
export const workflowExecutorOutputSchema = z.object({
result: z.any().optional(),
error: z.string().optional(),
pendingEvent: z.boolean().optional(),

View File

@ -1,16 +1,19 @@
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled';
import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled';
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
import {
WorkflowDiagramNode,
WorkflowDiagramStepNodeData,
WorkflowDiagramRunStatus,
WorkflowRunDiagramStepNodeData,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
import { WorkflowRunTabId } from '@/workflow/workflow-steps/types/WorkflowRunTabId';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import { isNull } from '@sniptt/guards';
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
import { useCallback } from 'react';
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
@ -24,9 +27,15 @@ export const WorkflowRunDiagramCanvasEffect = () => {
const workflowId = useRecoilValue(workflowIdState);
const goBackToFirstWorkflowRunRightDrawerTabIfNeeded = useRecoilCallback(
const resetWorkflowRunRightDrawerTabIfNeeded = useRecoilCallback(
({ snapshot, set }) =>
() => {
({
workflowSelectedNode,
stepExecutionStatus,
}: {
workflowSelectedNode: string;
stepExecutionStatus: WorkflowDiagramRunStatus;
}) => {
const activeWorkflowRunRightDrawerTab = getSnapshotValue(
snapshot,
activeTabIdComponentState.atomFamily({
@ -34,15 +43,40 @@ export const WorkflowRunDiagramCanvasEffect = () => {
}),
) as WorkflowRunTabId | null;
const isInputTabDisabled = getIsInputTabDisabled({
stepExecutionStatus,
workflowSelectedNode,
});
const isOutputTabDisabled = getIsOutputTabDisabled({
stepExecutionStatus,
});
if (isNull(activeWorkflowRunRightDrawerTab)) {
const defaultTabId = isOutputTabDisabled
? WorkflowRunTabId.NODE
: WorkflowRunTabId.OUTPUT;
set(
activeTabIdComponentState.atomFamily({
instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
}),
defaultTabId,
);
return;
}
if (
activeWorkflowRunRightDrawerTab === 'input' ||
activeWorkflowRunRightDrawerTab === 'output'
(isInputTabDisabled &&
activeWorkflowRunRightDrawerTab === WorkflowRunTabId.INPUT) ||
(isOutputTabDisabled &&
activeWorkflowRunRightDrawerTab === WorkflowRunTabId.OUTPUT)
) {
set(
activeTabIdComponentState.atomFamily({
instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
}),
'node',
WorkflowRunTabId.NODE,
);
}
},
@ -59,15 +93,8 @@ export const WorkflowRunDiagramCanvasEffect = () => {
setWorkflowSelectedNode(selectedNode.id);
const selectedNodeData = selectedNode.data as WorkflowDiagramStepNodeData;
if (
selectedNode.id === TRIGGER_STEP_ID ||
selectedNodeData.runStatus === 'not-executed' ||
selectedNodeData.runStatus === 'running'
) {
goBackToFirstWorkflowRunRightDrawerTabIfNeeded();
}
const selectedNodeData =
selectedNode.data as WorkflowRunDiagramStepNodeData;
if (isDefined(workflowId)) {
openWorkflowRunViewStepInCommandMenu(
@ -75,14 +102,19 @@ export const WorkflowRunDiagramCanvasEffect = () => {
selectedNodeData.name,
getIcon(getWorkflowNodeIconKey(selectedNodeData)),
);
resetWorkflowRunRightDrawerTabIfNeeded({
workflowSelectedNode: selectedNode.id,
stepExecutionStatus: selectedNodeData.runStatus,
});
}
},
[
setWorkflowSelectedNode,
resetWorkflowRunRightDrawerTabIfNeeded,
workflowId,
getIcon,
goBackToFirstWorkflowRunRightDrawerTabIfNeeded,
openWorkflowRunViewStepInCommandMenu,
getIcon,
],
);

View File

@ -1,30 +0,0 @@
import { useWorkflowRunUnsafe } from '@/workflow/hooks/useWorkflowRunUnsafe';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
import { CodeEditor } from 'twenty-ui/input';
const StyledSourceCodeContainer = styled.div`
margin: ${({ theme }) => theme.spacing(4)};
`;
export const WorkflowRunOutputVisualizer = ({
workflowRunId,
}: {
workflowRunId: string;
}) => {
const workflowRun = useWorkflowRunUnsafe({ workflowRunId });
if (!isDefined(workflowRun)) {
return null;
}
return (
<StyledSourceCodeContainer>
<CodeEditor
value={JSON.stringify(workflowRun.output, null, 2)}
language="json"
options={{ readOnly: true, domReadOnly: true }}
/>
</StyledSourceCodeContainer>
);
};

View File

@ -41,6 +41,13 @@ export type WorkflowDiagramStepNodeData =
runStatus?: WorkflowDiagramRunStatus;
};
export type WorkflowRunDiagramStepNodeData = Exclude<
WorkflowDiagramStepNodeData,
'runStatus'
> & {
runStatus: WorkflowDiagramRunStatus;
};
export type WorkflowDiagramCreateStepNodeData = {
nodeType: 'create-step';
parentNodeId: string;

View File

@ -19,25 +19,19 @@ export const getWorkflowDiagramTriggerNode = ({
switch (trigger.type) {
case 'MANUAL': {
triggerDefaultLabel = 'Manual Trigger';
triggerIcon = getTriggerIcon({
type: 'MANUAL',
});
triggerIcon = getTriggerIcon(trigger);
break;
}
case 'CRON': {
triggerDefaultLabel = 'On a Schedule';
triggerIcon = getTriggerIcon({
type: 'CRON',
});
triggerIcon = getTriggerIcon(trigger);
break;
}
case 'WEBHOOK': {
triggerDefaultLabel = 'Webhook';
triggerIcon = getTriggerIcon({
type: 'WEBHOOK',
});
triggerIcon = getTriggerIcon(trigger);
break;
}
@ -50,10 +44,7 @@ export const getWorkflowDiagramTriggerNode = ({
DATABASE_TRIGGER_TYPES.find((item) => item.event === triggerEvent.event)
?.defaultLabel ?? '';
triggerIcon = getTriggerIcon({
type: 'DATABASE_EVENT',
eventName: triggerEvent.event,
});
triggerIcon = getTriggerIcon(trigger);
break;
}

View File

@ -1,11 +1,15 @@
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
import { WorkflowExecutorOutput } from '@/workflow/types/Workflow';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
@ -30,24 +34,38 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
return null;
}
const stepOutput = workflowRun.output.stepsOutput[stepId];
const stepOutput = workflowRun.output.stepsOutput[stepId] as
| WorkflowExecutorOutput
| undefined;
const stepDefinition = getStepDefinitionOrThrow({
stepId,
trigger: workflowRun.output.flow.trigger,
steps: workflowRun.output.flow.steps,
});
if (stepDefinition?.type !== 'action') {
throw new Error('The output tab must be rendered with an action step.');
if (
!isDefined(stepDefinition?.definition) ||
!isDefined(stepDefinition.definition.name)
) {
throw new Error('The step is expected to be properly shaped.');
}
const headerTitle = stepDefinition.definition.name;
const headerIcon = getActionIcon(stepDefinition.definition.type);
const headerIconColor = getActionIconColorOrThrow({
theme,
actionType: stepDefinition.definition.type,
});
const headerType = getActionHeaderTypeOrThrow(stepDefinition.definition.type);
const headerIcon =
stepDefinition.type === 'trigger'
? getTriggerIcon(stepDefinition.definition)
: getActionIcon(stepDefinition.definition.type);
const headerIconColor =
stepDefinition.type === 'trigger'
? getTriggerIconColor({ theme })
: getActionIconColorOrThrow({
theme,
actionType: stepDefinition.definition.type,
});
const headerType =
stepDefinition.type === 'trigger'
? getTriggerHeaderType(stepDefinition.definition)
: i18n._(getActionHeaderTypeOrThrow(stepDefinition.definition.type));
const setRedHighlightingForEveryNode: GetJsonNodeHighlighting = () => 'red';
@ -58,12 +76,12 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={i18n._(headerType)}
headerType={headerType}
/>
<WorkflowRunStepJsonContainer>
<JsonTree
value={stepOutput}
value={stepOutput ?? t`No output available`}
shouldExpandNodeInitially={isTwoFirstDepths}
emptyArrayLabel={t`Empty Array`}
emptyObjectLabel={t`Empty Object`}
@ -71,7 +89,7 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
arrowButtonCollapsedLabel={t`Expand`}
arrowButtonExpandedLabel={t`Collapse`}
getNodeHighlighting={
isDefined(stepOutput.error)
isDefined(stepOutput?.error)
? setRedHighlightingForEveryNode
: undefined
}

View File

@ -1,5 +1,6 @@
import { WorkflowRunOutput } from '@/workflow/types/Workflow';
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import { isNull } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
@ -14,6 +15,10 @@ export const getWorkflowRunStepExecutionStatus = ({
return 'not-executed';
}
if (stepId === TRIGGER_STEP_ID) {
return 'success';
}
const stepOutput = workflowRunOutput.stepsOutput?.[stepId];
if (isDefined(stepOutput?.error)) {

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;
};

View File

@ -28,11 +28,7 @@ export const getTriggerStepName = (trigger: WorkflowTrigger): string => {
const getDatabaseEventTriggerStepName = (
trigger: WorkflowDatabaseEventTrigger,
): string => {
const [, action] = trigger.settings.eventName.split('.');
const defaultLabel = getTriggerDefaultLabel({
type: 'DATABASE_EVENT',
eventName: action,
});
const defaultLabel = getTriggerDefaultLabel(trigger);
return defaultLabel ?? '';
};