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

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