Visualize workflow run step input (#10677)
- Compute the context the selected step had access to during its execution and display it with the `<JsonNestedNode />` component - Ensure several steps with the same name can be displayed in order - Prevent access to the input tab in a few cases - Hide the input tab when the trigger node is selected as this node takes no input - Hide the input tab when the selected node has not been executed yet or is currently executed - Fallback to the Node tab when the Input tab can't be accessed ## Successful workflow execution https://github.com/user-attachments/assets/4a2bb5f5-450c-46ed-b2d7-a14d3b1e5c1f ## Failed workflow execution https://github.com/user-attachments/assets/3be2784e-e76c-48ab-aef5-17f63410898e Closes https://github.com/twentyhq/core-team-issues/issues/433
This commit is contained in:
committed by
GitHub
parent
9d78dc322d
commit
cb5f4820d7
@ -10,6 +10,7 @@ import { CardType } from '@/object-record/record-show/types/CardType';
|
|||||||
import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
|
import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
|
||||||
import { WorkflowRunOutputVisualizer } from '@/workflow/components/WorkflowRunOutputVisualizer';
|
import { WorkflowRunOutputVisualizer } from '@/workflow/components/WorkflowRunOutputVisualizer';
|
||||||
import { WorkflowRunVisualizer } from '@/workflow/components/WorkflowRunVisualizer';
|
import { WorkflowRunVisualizer } from '@/workflow/components/WorkflowRunVisualizer';
|
||||||
|
import { WorkflowRunVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect';
|
||||||
import { WorkflowVersionVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizer';
|
import { WorkflowVersionVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizer';
|
||||||
import { WorkflowVersionVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect';
|
import { WorkflowVersionVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect';
|
||||||
import { WorkflowVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVisualizer';
|
import { WorkflowVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVisualizer';
|
||||||
@ -94,7 +95,11 @@ export const CardComponents: Record<CardType, CardComponentType> = {
|
|||||||
),
|
),
|
||||||
|
|
||||||
[CardType.WorkflowRunCard]: ({ targetableObject }) => (
|
[CardType.WorkflowRunCard]: ({ targetableObject }) => (
|
||||||
<WorkflowRunVisualizer workflowRunId={targetableObject.id} />
|
<>
|
||||||
|
<WorkflowRunVisualizerEffect workflowRunId={targetableObject.id} />
|
||||||
|
|
||||||
|
<WorkflowRunVisualizer workflowRunId={targetableObject.id} />
|
||||||
|
</>
|
||||||
),
|
),
|
||||||
[CardType.WorkflowRunOutputCard]: ({ targetableObject }) => (
|
[CardType.WorkflowRunOutputCard]: ({ targetableObject }) => (
|
||||||
<WorkflowRunOutputVisualizer workflowRunId={targetableObject.id} />
|
<WorkflowRunOutputVisualizer workflowRunId={targetableObject.id} />
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
||||||
import { WorkflowRun } from '@/workflow/types/Workflow';
|
import { WorkflowRun } from '@/workflow/types/Workflow';
|
||||||
import { WorkflowRunDiagramCanvas } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas';
|
import { WorkflowRunDiagramCanvas } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas';
|
||||||
import { WorkflowRunVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect';
|
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
export const WorkflowRunVisualizerContent = ({
|
export const WorkflowRunVisualizerContent = ({
|
||||||
@ -14,11 +13,5 @@ export const WorkflowRunVisualizerContent = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <WorkflowRunDiagramCanvas versionStatus={workflowVersion.status} />;
|
||||||
<>
|
|
||||||
<WorkflowRunVisualizerEffect workflowRun={workflowRun} />
|
|
||||||
|
|
||||||
<WorkflowRunDiagramCanvas versionStatus={workflowVersion.status} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,7 +14,8 @@ export const JsonArrayNode = ({
|
|||||||
return (
|
return (
|
||||||
<JsonNestedNode
|
<JsonNestedNode
|
||||||
elements={[...value.entries()].map(([key, value]) => ({
|
elements={[...value.entries()].map(([key, value]) => ({
|
||||||
key: String(key),
|
id: key,
|
||||||
|
label: String(key),
|
||||||
value,
|
value,
|
||||||
}))}
|
}))}
|
||||||
label={label}
|
label={label}
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export const JsonNestedNode = ({
|
|||||||
}: {
|
}: {
|
||||||
label?: string;
|
label?: string;
|
||||||
Icon: IconComponent;
|
Icon: IconComponent;
|
||||||
elements: Array<{ key: string; value: JsonValue }>;
|
elements: Array<{ id: string | number; label: string; value: JsonValue }>;
|
||||||
depth: number;
|
depth: number;
|
||||||
}) => {
|
}) => {
|
||||||
const hideRoot = !isDefined(label);
|
const hideRoot = !isDefined(label);
|
||||||
@ -37,8 +37,8 @@ export const JsonNestedNode = ({
|
|||||||
|
|
||||||
const renderedChildren = (
|
const renderedChildren = (
|
||||||
<JsonList depth={depth}>
|
<JsonList depth={depth}>
|
||||||
{elements.map(({ key, value }) => (
|
{elements.map(({ id, label, value }) => (
|
||||||
<JsonNode key={key} label={key} value={value} depth={depth + 1} />
|
<JsonNode key={id} label={label} value={value} depth={depth + 1} />
|
||||||
))}
|
))}
|
||||||
</JsonList>
|
</JsonList>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -14,7 +14,8 @@ export const JsonObjectNode = ({
|
|||||||
return (
|
return (
|
||||||
<JsonNestedNode
|
<JsonNestedNode
|
||||||
elements={Object.entries(value).map(([key, value]) => ({
|
elements={Object.entries(value).map(([key, value]) => ({
|
||||||
key,
|
id: key,
|
||||||
|
label: key,
|
||||||
value,
|
value,
|
||||||
}))}
|
}))}
|
||||||
label={label}
|
label={label}
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { workflowRunIdState } from '@/workflow/states/workflowRunIdState';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
|
export const useWorkflowRunIdOrThrow = () => {
|
||||||
|
const workflowRunId = useRecoilValue(workflowRunIdState);
|
||||||
|
if (!isDefined(workflowRunId)) {
|
||||||
|
throw new Error('Expected the workflow run ID to be defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
return workflowRunId;
|
||||||
|
};
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { createState } from '@ui/utilities/state/utils/createState';
|
||||||
|
|
||||||
|
export const workflowRunIdState = createState<string | undefined>({
|
||||||
|
key: 'workflowRunIdState',
|
||||||
|
defaultValue: undefined,
|
||||||
|
});
|
||||||
@ -11,7 +11,9 @@ import {
|
|||||||
workflowFindRecordsActionSchema,
|
workflowFindRecordsActionSchema,
|
||||||
workflowFindRecordsActionSettingsSchema,
|
workflowFindRecordsActionSettingsSchema,
|
||||||
workflowManualTriggerSchema,
|
workflowManualTriggerSchema,
|
||||||
|
workflowRunContextSchema,
|
||||||
workflowRunOutputSchema,
|
workflowRunOutputSchema,
|
||||||
|
workflowRunOutputStepsOutputSchema,
|
||||||
workflowRunSchema,
|
workflowRunSchema,
|
||||||
workflowSendEmailActionSchema,
|
workflowSendEmailActionSchema,
|
||||||
workflowSendEmailActionSettingsSchema,
|
workflowSendEmailActionSettingsSchema,
|
||||||
@ -97,7 +99,13 @@ export type WorkflowVersion = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowRunOutput = z.infer<typeof workflowRunOutputSchema>;
|
export type WorkflowRunOutput = z.infer<typeof workflowRunOutputSchema>;
|
||||||
export type WorkflowRunOutputStepsOutput = WorkflowRunOutput['stepsOutput'];
|
export type WorkflowRunOutputStepsOutput = z.infer<
|
||||||
|
typeof workflowRunOutputStepsOutputSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type WorkflowRunContext = z.infer<typeof workflowRunContextSchema>;
|
||||||
|
|
||||||
|
export type WorkflowRunFlow = WorkflowRunOutput['flow'];
|
||||||
|
|
||||||
export type WorkflowRun = z.infer<typeof workflowRunSchema>;
|
export type WorkflowRun = z.infer<typeof workflowRunSchema>;
|
||||||
|
|
||||||
|
|||||||
@ -181,7 +181,7 @@ const workflowExecutorOutputSchema = z.object({
|
|||||||
error: z.string().optional(),
|
error: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const workflowRunOutputStepsOutputSchema = z.record(
|
export const workflowRunOutputStepsOutputSchema = z.record(
|
||||||
workflowExecutorOutputSchema,
|
workflowExecutorOutputSchema,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -195,11 +195,18 @@ export const workflowRunOutputSchema = z.object({
|
|||||||
error: z.string().optional(),
|
error: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const workflowRunSchema = z.object({
|
export const workflowRunContextSchema = z.record(z.any());
|
||||||
__typename: z.literal('WorkflowRun'),
|
|
||||||
id: z.string(),
|
|
||||||
workflowVersionId: z.string(),
|
|
||||||
output: workflowRunOutputSchema.nullable(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type WorkflowRunOutput = z.infer<typeof workflowRunOutputSchema>;
|
export const workflowRunSchema = z
|
||||||
|
.object({
|
||||||
|
__typename: z.literal('WorkflowRun'),
|
||||||
|
id: z.string(),
|
||||||
|
workflowVersionId: z.string(),
|
||||||
|
output: workflowRunOutputSchema.nullable(),
|
||||||
|
context: workflowRunContextSchema.nullable(),
|
||||||
|
createdAt: z.string(),
|
||||||
|
deletedAt: z.string().nullable(),
|
||||||
|
endedAt: z.string().nullable(),
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|||||||
@ -2,16 +2,21 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
|||||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||||
|
import { useTabListStates } from '@/ui/layout/tab/hooks/internal/useTabListStates';
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
|
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||||
import {
|
import {
|
||||||
WorkflowDiagramNode,
|
WorkflowDiagramNode,
|
||||||
WorkflowDiagramStepNodeData,
|
WorkflowDiagramStepNodeData,
|
||||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
|
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 { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { useIcons } from 'twenty-ui';
|
import { useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
@ -22,6 +27,26 @@ export const WorkflowRunDiagramCanvasEffect = () => {
|
|||||||
const setHotkeyScope = useSetHotkeyScope();
|
const setHotkeyScope = useSetHotkeyScope();
|
||||||
const { closeCommandMenu } = useCommandMenu();
|
const { closeCommandMenu } = useCommandMenu();
|
||||||
|
|
||||||
|
const { activeTabIdState: workflowRunRightDrawerListActiveTabIdState } =
|
||||||
|
useTabListStates({
|
||||||
|
tabListScopeId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
||||||
|
});
|
||||||
|
|
||||||
|
const goBackToFirstWorkflowRunRightDrawerTabIfNeeded = useRecoilCallback(
|
||||||
|
({ snapshot, set }) =>
|
||||||
|
() => {
|
||||||
|
const activeWorkflowRunRightDrawerTab = getSnapshotValue(
|
||||||
|
snapshot,
|
||||||
|
workflowRunRightDrawerListActiveTabIdState,
|
||||||
|
) as WorkflowRunTabId | null;
|
||||||
|
|
||||||
|
if (activeWorkflowRunRightDrawerTab === 'input') {
|
||||||
|
set(workflowRunRightDrawerListActiveTabIdState, 'node');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[workflowRunRightDrawerListActiveTabIdState],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelectionChange = useCallback(
|
const handleSelectionChange = useCallback(
|
||||||
({ nodes }: OnSelectionChangeParams) => {
|
({ nodes }: OnSelectionChangeParams) => {
|
||||||
const selectedNode = nodes[0] as WorkflowDiagramNode;
|
const selectedNode = nodes[0] as WorkflowDiagramNode;
|
||||||
@ -38,6 +63,14 @@ export const WorkflowRunDiagramCanvasEffect = () => {
|
|||||||
|
|
||||||
const selectedNodeData = selectedNode.data as WorkflowDiagramStepNodeData;
|
const selectedNodeData = selectedNode.data as WorkflowDiagramStepNodeData;
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedNode.id === TRIGGER_STEP_ID ||
|
||||||
|
selectedNodeData.runStatus === 'not-executed' ||
|
||||||
|
selectedNodeData.runStatus === 'running'
|
||||||
|
) {
|
||||||
|
goBackToFirstWorkflowRunRightDrawerTabIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
openRightDrawer(RightDrawerPages.WorkflowRunStepView, {
|
openRightDrawer(RightDrawerPages.WorkflowRunStepView, {
|
||||||
title: selectedNodeData.name,
|
title: selectedNodeData.name,
|
||||||
Icon: getIcon(getWorkflowNodeIconKey(selectedNodeData)),
|
Icon: getIcon(getWorkflowNodeIconKey(selectedNodeData)),
|
||||||
@ -47,9 +80,10 @@ export const WorkflowRunDiagramCanvasEffect = () => {
|
|||||||
setWorkflowSelectedNode,
|
setWorkflowSelectedNode,
|
||||||
setHotkeyScope,
|
setHotkeyScope,
|
||||||
openRightDrawer,
|
openRightDrawer,
|
||||||
|
getIcon,
|
||||||
closeRightDrawer,
|
closeRightDrawer,
|
||||||
closeCommandMenu,
|
closeCommandMenu,
|
||||||
getIcon,
|
goBackToFirstWorkflowRunRightDrawerTabIfNeeded,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
|
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||||
import { flowState } from '@/workflow/states/flowState';
|
import { flowState } from '@/workflow/states/flowState';
|
||||||
import { WorkflowRun } from '@/workflow/types/Workflow';
|
import { workflowRunIdState } from '@/workflow/states/workflowRunIdState';
|
||||||
import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflowDiagramState';
|
import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflowDiagramState';
|
||||||
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
|
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@ -7,16 +8,24 @@ import { useSetRecoilState } from 'recoil';
|
|||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
export const WorkflowRunVisualizerEffect = ({
|
export const WorkflowRunVisualizerEffect = ({
|
||||||
workflowRun,
|
workflowRunId,
|
||||||
}: {
|
}: {
|
||||||
workflowRun: WorkflowRun;
|
workflowRunId: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||||
|
|
||||||
|
const setWorkflowRunId = useSetRecoilState(workflowRunIdState);
|
||||||
const setFlow = useSetRecoilState(flowState);
|
const setFlow = useSetRecoilState(flowState);
|
||||||
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDefined(workflowRun.output)) {
|
setWorkflowRunId(workflowRunId);
|
||||||
|
}, [setWorkflowRunId, workflowRunId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDefined(workflowRun?.output)) {
|
||||||
setFlow(undefined);
|
setFlow(undefined);
|
||||||
|
setWorkflowDiagram(undefined);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -25,14 +34,6 @@ export const WorkflowRunVisualizerEffect = ({
|
|||||||
trigger: workflowRun.output.flow.trigger,
|
trigger: workflowRun.output.flow.trigger,
|
||||||
steps: workflowRun.output.flow.steps,
|
steps: workflowRun.output.flow.steps,
|
||||||
});
|
});
|
||||||
}, [setFlow, workflowRun.output]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isDefined(workflowRun.output)) {
|
|
||||||
setWorkflowDiagram(undefined);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextWorkflowDiagram = generateWorkflowRunDiagram({
|
const nextWorkflowDiagram = generateWorkflowRunDiagram({
|
||||||
trigger: workflowRun.output.flow.trigger,
|
trigger: workflowRun.output.flow.trigger,
|
||||||
@ -41,7 +42,7 @@ export const WorkflowRunVisualizerEffect = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setWorkflowDiagram(nextWorkflowDiagram);
|
setWorkflowDiagram(nextWorkflowDiagram);
|
||||||
}, [setWorkflowDiagram, workflowRun.output]);
|
}, [setFlow, setWorkflowDiagram, workflowRun?.output]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,10 +2,16 @@ import { ShowPageSubContainerTabListContainer } from '@/ui/layout/show-page/comp
|
|||||||
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
|
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
|
||||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||||
|
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||||
|
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||||
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
|
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
|
||||||
|
import { WorkflowRunStepInputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepInputDetail';
|
||||||
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
|
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
|
||||||
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
|
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
|
||||||
|
import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus';
|
||||||
|
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui';
|
import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledTabListContainer = styled(ShowPageSubContainerTabListContainer)`
|
const StyledTabListContainer = styled(ShowPageSubContainerTabListContainer)`
|
||||||
@ -17,17 +23,41 @@ type TabId = 'node' | 'input' | 'output';
|
|||||||
export const RightDrawerWorkflowRunViewStep = () => {
|
export const RightDrawerWorkflowRunViewStep = () => {
|
||||||
const flow = useFlowOrThrow();
|
const flow = useFlowOrThrow();
|
||||||
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
|
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
|
||||||
|
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||||
|
|
||||||
|
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||||
|
|
||||||
const { activeTabId } = useTabList<TabId>(
|
const { activeTabId } = useTabList<TabId>(
|
||||||
WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const stepExecutionStatus = isDefined(workflowRun)
|
||||||
|
? getWorkflowRunStepExecutionStatus({
|
||||||
|
workflowRunOutput: workflowRun.output,
|
||||||
|
stepId: workflowSelectedNode,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const isInputTabDisabled =
|
||||||
|
workflowSelectedNode === TRIGGER_STEP_ID ||
|
||||||
|
stepExecutionStatus === 'running' ||
|
||||||
|
stepExecutionStatus === 'not-executed';
|
||||||
|
|
||||||
const tabs: SingleTabProps<TabId>[] = [
|
const tabs: SingleTabProps<TabId>[] = [
|
||||||
{ id: 'node', title: 'Node', Icon: IconStepInto },
|
{ id: 'node', title: 'Node', Icon: IconStepInto },
|
||||||
{ id: 'input', title: 'Input', Icon: IconLogin2 },
|
{
|
||||||
|
id: 'input',
|
||||||
|
title: 'Input',
|
||||||
|
Icon: IconLogin2,
|
||||||
|
disabled: isInputTabDisabled,
|
||||||
|
},
|
||||||
{ id: 'output', title: 'Output', Icon: IconLogout },
|
{ id: 'output', title: 'Output', Icon: IconLogout },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!isDefined(workflowRun)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledTabListContainer>
|
<StyledTabListContainer>
|
||||||
@ -46,6 +76,10 @@ export const RightDrawerWorkflowRunViewStep = () => {
|
|||||||
steps={flow.steps}
|
steps={flow.steps}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{activeTabId === 'input' ? (
|
||||||
|
<WorkflowRunStepInputDetail stepId={workflowSelectedNode} />
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,47 @@
|
|||||||
|
import { JsonNestedNode } from '@/workflow/components/json-visualizer/components/JsonNestedNode';
|
||||||
|
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||||
|
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||||
|
import { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
import { IconBrackets } from 'twenty-ui';
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
padding-block: ${({ theme }) => theme.spacing(4)};
|
||||||
|
padding-inline: ${({ theme }) => theme.spacing(3)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||||
|
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||||
|
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
isDefined(workflowRun) &&
|
||||||
|
isDefined(workflowRun.context) &&
|
||||||
|
isDefined(workflowRun.output?.flow)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepContext = getWorkflowRunStepContext({
|
||||||
|
context: workflowRun.context,
|
||||||
|
flow: workflowRun.output.flow,
|
||||||
|
stepId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<JsonNestedNode
|
||||||
|
elements={stepContext.map(({ id, name, context }) => ({
|
||||||
|
id,
|
||||||
|
label: name,
|
||||||
|
value: context,
|
||||||
|
}))}
|
||||||
|
Icon={IconBrackets}
|
||||||
|
depth={0}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,142 @@
|
|||||||
|
import { flowState } from '@/workflow/states/flowState';
|
||||||
|
import { workflowRunIdState } from '@/workflow/states/workflowRunIdState';
|
||||||
|
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||||
|
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||||
|
import { graphql, HttpResponse } from 'msw';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
|
||||||
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
|
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||||
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
|
import { oneFailedWorkflowRunQueryResult } from '~/testing/mock-data/workflow-run';
|
||||||
|
import { RightDrawerWorkflowRunViewStep } from '../RightDrawerWorkflowRunViewStep';
|
||||||
|
|
||||||
|
const StyledWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 500px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const meta: Meta<typeof RightDrawerWorkflowRunViewStep> = {
|
||||||
|
title: 'Modules/Workflow/RightDrawerWorkflowRunViewStep',
|
||||||
|
component: RightDrawerWorkflowRunViewStep,
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<StyledWrapper>
|
||||||
|
<Story />
|
||||||
|
</StyledWrapper>
|
||||||
|
),
|
||||||
|
I18nFrontDecorator,
|
||||||
|
ComponentDecorator,
|
||||||
|
(Story) => {
|
||||||
|
const setFlow = useSetRecoilState(flowState);
|
||||||
|
const setWorkflowSelectedNode = useSetRecoilState(
|
||||||
|
workflowSelectedNodeState,
|
||||||
|
);
|
||||||
|
const setWorkflowRunId = useSetRecoilState(workflowRunIdState);
|
||||||
|
|
||||||
|
setFlow(oneFailedWorkflowRunQueryResult.workflowRun.output.flow);
|
||||||
|
setWorkflowSelectedNode(
|
||||||
|
oneFailedWorkflowRunQueryResult.workflowRun.output.flow.steps[0].id,
|
||||||
|
);
|
||||||
|
setWorkflowRunId(oneFailedWorkflowRunQueryResult.workflowRun.id);
|
||||||
|
|
||||||
|
return <Story />;
|
||||||
|
},
|
||||||
|
RouterDecorator,
|
||||||
|
ObjectMetadataItemsDecorator,
|
||||||
|
WorkspaceDecorator,
|
||||||
|
],
|
||||||
|
parameters: {
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
graphql.query('FindOneWorkflowRun', () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
data: oneFailedWorkflowRunQueryResult,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
...graphqlMocks.handlers,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof RightDrawerWorkflowRunViewStep>;
|
||||||
|
|
||||||
|
export const NodeTab: Story = {};
|
||||||
|
|
||||||
|
export const InputTab: Story = {
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await userEvent.click(await canvas.findByRole('button', { name: 'Input' }));
|
||||||
|
|
||||||
|
expect(await canvas.findByText('Trigger')).toBeVisible();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InputTabDisabledForTrigger: Story = {
|
||||||
|
decorators: [
|
||||||
|
(Story) => {
|
||||||
|
const setWorkflowSelectedNode = useSetRecoilState(
|
||||||
|
workflowSelectedNodeState,
|
||||||
|
);
|
||||||
|
|
||||||
|
setWorkflowSelectedNode(TRIGGER_STEP_ID);
|
||||||
|
|
||||||
|
return <Story />;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const inputTab = await canvas.findByRole('button', { name: 'Input' });
|
||||||
|
|
||||||
|
expect(inputTab).toBeDisabled();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InputTabNotExecutedStep: Story = {
|
||||||
|
decorators: [
|
||||||
|
(Story) => {
|
||||||
|
const setWorkflowSelectedNode = useSetRecoilState(
|
||||||
|
workflowSelectedNodeState,
|
||||||
|
);
|
||||||
|
|
||||||
|
setWorkflowSelectedNode(
|
||||||
|
oneFailedWorkflowRunQueryResult.workflowRun.output.flow.steps.at(-1)!
|
||||||
|
.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Story />;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const inputTab = await canvas.findByRole('button', { name: 'Input' });
|
||||||
|
|
||||||
|
expect(inputTab).toBeDisabled();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OutputTab: Story = {
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await userEvent.click(
|
||||||
|
await canvas.findByRole('button', { name: 'Output' }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(canvas.queryByText('Create Record')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export type WorkflowRunTabId = 'node' | 'input' | 'output';
|
||||||
@ -0,0 +1,266 @@
|
|||||||
|
import { WorkflowRunFlow } from '@/workflow/types/Workflow';
|
||||||
|
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||||
|
import { getWorkflowRunStepContext } from '../getWorkflowRunStepContext';
|
||||||
|
|
||||||
|
describe('getWorkflowRunStepContext', () => {
|
||||||
|
it('should return an empty array for trigger step', () => {
|
||||||
|
const flow = {
|
||||||
|
trigger: {
|
||||||
|
name: 'Company Created',
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
settings: {
|
||||||
|
eventName: 'company.created',
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
steps: [],
|
||||||
|
} satisfies WorkflowRunFlow;
|
||||||
|
const context = {
|
||||||
|
[TRIGGER_STEP_ID]: { company: { id: '123' } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkflowRunStepContext({
|
||||||
|
stepId: TRIGGER_STEP_ID,
|
||||||
|
flow,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include previous steps context', () => {
|
||||||
|
const flow = {
|
||||||
|
trigger: {
|
||||||
|
name: 'Company Created',
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
settings: {
|
||||||
|
eventName: 'company.created',
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: 'step1',
|
||||||
|
name: 'Create company',
|
||||||
|
type: 'CREATE_RECORD',
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
retryOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
objectName: 'Company',
|
||||||
|
objectRecord: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step2',
|
||||||
|
name: 'Send Email',
|
||||||
|
type: 'SEND_EMAIL',
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
retryOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
connectedAccountId: '123',
|
||||||
|
email: '',
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} satisfies WorkflowRunFlow;
|
||||||
|
const context = {
|
||||||
|
[TRIGGER_STEP_ID]: { company: { id: '123' } },
|
||||||
|
step1: { taskId: '456' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkflowRunStepContext({
|
||||||
|
stepId: 'step2',
|
||||||
|
flow,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
id: TRIGGER_STEP_ID,
|
||||||
|
name: 'Company Created',
|
||||||
|
context: { company: { id: '123' } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step1',
|
||||||
|
name: 'Create company',
|
||||||
|
context: { taskId: '456' },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include subsequent steps context', () => {
|
||||||
|
const flow = {
|
||||||
|
trigger: {
|
||||||
|
name: 'Company Created',
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
settings: {
|
||||||
|
eventName: 'company.created',
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: 'step1',
|
||||||
|
name: 'Create company',
|
||||||
|
type: 'CREATE_RECORD',
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
retryOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
objectName: 'Company',
|
||||||
|
objectRecord: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step2',
|
||||||
|
name: 'Send Email',
|
||||||
|
type: 'SEND_EMAIL',
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
retryOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
connectedAccountId: '123',
|
||||||
|
email: '',
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} satisfies WorkflowRunFlow;
|
||||||
|
const context = {
|
||||||
|
[TRIGGER_STEP_ID]: { company: { id: '123' } },
|
||||||
|
step1: { taskId: '456' },
|
||||||
|
step2: { emailId: '789' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkflowRunStepContext({
|
||||||
|
stepId: 'step1',
|
||||||
|
flow,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
id: TRIGGER_STEP_ID,
|
||||||
|
name: 'Company Created',
|
||||||
|
context: { company: { id: '123' } },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple steps with the same name', () => {
|
||||||
|
const flow = {
|
||||||
|
trigger: {
|
||||||
|
name: 'Company Created',
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
settings: {
|
||||||
|
eventName: 'company.created',
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: 'step1',
|
||||||
|
name: 'Create Note',
|
||||||
|
type: 'CREATE_RECORD',
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
retryOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
objectName: 'Note',
|
||||||
|
objectRecord: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step2',
|
||||||
|
name: 'Create Note',
|
||||||
|
type: 'CREATE_RECORD',
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
retryOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
objectName: 'Note',
|
||||||
|
objectRecord: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step3',
|
||||||
|
name: 'Create Note',
|
||||||
|
type: 'CREATE_RECORD',
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
retryOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
objectName: 'Note',
|
||||||
|
objectRecord: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} satisfies WorkflowRunFlow;
|
||||||
|
const context = {
|
||||||
|
[TRIGGER_STEP_ID]: { company: { id: '123' } },
|
||||||
|
step1: { noteId: '456' },
|
||||||
|
step2: { noteId: '789' },
|
||||||
|
step3: { noteId: '101' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkflowRunStepContext({
|
||||||
|
stepId: 'step3',
|
||||||
|
flow,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
id: TRIGGER_STEP_ID,
|
||||||
|
name: 'Company Created',
|
||||||
|
context: { company: { id: '123' } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step1',
|
||||||
|
name: 'Create Note',
|
||||||
|
context: { noteId: '456' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step2',
|
||||||
|
name: 'Create Note',
|
||||||
|
context: { noteId: '789' },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,230 @@
|
|||||||
|
import { getWorkflowRunStepExecutionStatus } from '../getWorkflowRunStepExecutionStatus';
|
||||||
|
|
||||||
|
describe('getWorkflowRunStepExecutionStatus', () => {
|
||||||
|
const stepId = '453e0084-aca2-45b9-8d1c-458a2b8ac70a';
|
||||||
|
|
||||||
|
it('should return not-executed when the output is null', () => {
|
||||||
|
expect(
|
||||||
|
getWorkflowRunStepExecutionStatus({
|
||||||
|
workflowRunOutput: null,
|
||||||
|
stepId,
|
||||||
|
}),
|
||||||
|
).toBe('not-executed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success when step has result', () => {
|
||||||
|
expect(
|
||||||
|
getWorkflowRunStepExecutionStatus({
|
||||||
|
workflowRunOutput: {
|
||||||
|
flow: {
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: stepId,
|
||||||
|
name: 'Code - Serverless Function',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: false,
|
||||||
|
settings: {
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId:
|
||||||
|
'5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c',
|
||||||
|
serverlessFunctionInput: {
|
||||||
|
a: null,
|
||||||
|
b: null,
|
||||||
|
},
|
||||||
|
serverlessFunctionVersion: 'draft',
|
||||||
|
},
|
||||||
|
outputSchema: {
|
||||||
|
link: {
|
||||||
|
tab: 'test',
|
||||||
|
icon: 'IconVariable',
|
||||||
|
label: 'Generate Function Output',
|
||||||
|
isLeaf: true,
|
||||||
|
},
|
||||||
|
_outputSchemaType: 'LINK',
|
||||||
|
},
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
continueOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
trigger: {
|
||||||
|
type: 'MANUAL',
|
||||||
|
settings: {
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stepsOutput: {
|
||||||
|
[stepId]: {
|
||||||
|
result: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stepId,
|
||||||
|
}),
|
||||||
|
).toBe('success');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return failure when workflow has error', () => {
|
||||||
|
const error = 'fn(...).then is not a function';
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getWorkflowRunStepExecutionStatus({
|
||||||
|
workflowRunOutput: {
|
||||||
|
flow: {
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: stepId,
|
||||||
|
name: 'Code - Serverless Function',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: false,
|
||||||
|
settings: {
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId:
|
||||||
|
'5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c',
|
||||||
|
serverlessFunctionInput: {
|
||||||
|
a: null,
|
||||||
|
b: null,
|
||||||
|
},
|
||||||
|
serverlessFunctionVersion: 'draft',
|
||||||
|
},
|
||||||
|
outputSchema: {
|
||||||
|
link: {
|
||||||
|
tab: 'test',
|
||||||
|
icon: 'IconVariable',
|
||||||
|
label: 'Generate Function Output',
|
||||||
|
isLeaf: true,
|
||||||
|
},
|
||||||
|
_outputSchemaType: 'LINK',
|
||||||
|
},
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
continueOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
trigger: {
|
||||||
|
type: 'MANUAL',
|
||||||
|
settings: {
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error,
|
||||||
|
stepsOutput: {
|
||||||
|
[stepId]: {
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stepId,
|
||||||
|
}),
|
||||||
|
).toBe('failure');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return not-executed when step has no output', () => {
|
||||||
|
const secondStepId = '5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c';
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getWorkflowRunStepExecutionStatus({
|
||||||
|
workflowRunOutput: {
|
||||||
|
flow: {
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: stepId,
|
||||||
|
name: 'Code - Serverless Function',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: false,
|
||||||
|
settings: {
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId:
|
||||||
|
'5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c',
|
||||||
|
serverlessFunctionInput: {
|
||||||
|
a: null,
|
||||||
|
b: null,
|
||||||
|
},
|
||||||
|
serverlessFunctionVersion: 'draft',
|
||||||
|
},
|
||||||
|
outputSchema: {
|
||||||
|
link: {
|
||||||
|
tab: 'test',
|
||||||
|
icon: 'IconVariable',
|
||||||
|
label: 'Generate Function Output',
|
||||||
|
isLeaf: true,
|
||||||
|
},
|
||||||
|
_outputSchemaType: 'LINK',
|
||||||
|
},
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
continueOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: secondStepId,
|
||||||
|
name: 'Code - Serverless Function',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: false,
|
||||||
|
settings: {
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId:
|
||||||
|
'5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c',
|
||||||
|
serverlessFunctionInput: {
|
||||||
|
a: null,
|
||||||
|
b: null,
|
||||||
|
},
|
||||||
|
serverlessFunctionVersion: 'draft',
|
||||||
|
},
|
||||||
|
outputSchema: {
|
||||||
|
link: {
|
||||||
|
tab: 'test',
|
||||||
|
icon: 'IconVariable',
|
||||||
|
label: 'Generate Function Output',
|
||||||
|
isLeaf: true,
|
||||||
|
},
|
||||||
|
_outputSchemaType: 'LINK',
|
||||||
|
},
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
continueOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
trigger: {
|
||||||
|
type: 'MANUAL',
|
||||||
|
settings: {
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stepsOutput: {
|
||||||
|
[stepId]: {
|
||||||
|
result: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stepId: secondStepId,
|
||||||
|
}),
|
||||||
|
).toBe('not-executed');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { WorkflowRunContext, WorkflowRunFlow } from '@/workflow/types/Workflow';
|
||||||
|
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||||
|
|
||||||
|
export const getWorkflowRunStepContext = ({
|
||||||
|
stepId,
|
||||||
|
flow,
|
||||||
|
context,
|
||||||
|
}: {
|
||||||
|
stepId: string;
|
||||||
|
context: WorkflowRunContext;
|
||||||
|
flow: WorkflowRunFlow;
|
||||||
|
}) => {
|
||||||
|
const stepContext: Array<{ id: string; name: string; context: any }> = [];
|
||||||
|
|
||||||
|
if (stepId === TRIGGER_STEP_ID) {
|
||||||
|
return stepContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
stepContext.push({
|
||||||
|
id: TRIGGER_STEP_ID,
|
||||||
|
name: flow.trigger.name ?? 'Trigger',
|
||||||
|
context: context[TRIGGER_STEP_ID],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const step of flow.steps) {
|
||||||
|
if (step.id === stepId) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
stepContext.push({
|
||||||
|
id: step.id,
|
||||||
|
name: step.name,
|
||||||
|
context: context[step.id],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return stepContext;
|
||||||
|
};
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { WorkflowRunOutput } from '@/workflow/types/Workflow';
|
||||||
|
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { isNull } from '@sniptt/guards';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
|
export const getWorkflowRunStepExecutionStatus = ({
|
||||||
|
workflowRunOutput,
|
||||||
|
stepId,
|
||||||
|
}: {
|
||||||
|
workflowRunOutput: WorkflowRunOutput | null;
|
||||||
|
stepId: string;
|
||||||
|
}): WorkflowDiagramRunStatus => {
|
||||||
|
if (isNull(workflowRunOutput)) {
|
||||||
|
return 'not-executed';
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepOutput = workflowRunOutput.stepsOutput?.[stepId];
|
||||||
|
|
||||||
|
if (isDefined(stepOutput?.error)) {
|
||||||
|
return 'failure';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(stepOutput?.result)) {
|
||||||
|
return 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'not-executed';
|
||||||
|
};
|
||||||
@ -1,8 +1,13 @@
|
|||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
import {
|
||||||
|
WorkflowTriggerType,
|
||||||
|
WorkflowWithCurrentVersion,
|
||||||
|
} from '@/workflow/types/Workflow';
|
||||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||||
|
import { RightDrawerStepListContainer } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepContainer';
|
||||||
|
import { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepTitle';
|
||||||
import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes';
|
import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes';
|
||||||
import { OTHER_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/OtherTriggerTypes';
|
import { OTHER_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/OtherTriggerTypes';
|
||||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||||
@ -10,8 +15,6 @@ import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hoo
|
|||||||
import { getTriggerDefaultDefinition } from '@/workflow/workflow-trigger/utils/getTriggerDefaultDefinition';
|
import { getTriggerDefaultDefinition } from '@/workflow/workflow-trigger/utils/getTriggerDefaultDefinition';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { MenuItemCommand, useIcons } from 'twenty-ui';
|
import { MenuItemCommand, useIcons } from 'twenty-ui';
|
||||||
import { RightDrawerStepListContainer } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepContainer';
|
|
||||||
import { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepTitle';
|
|
||||||
|
|
||||||
export const RightDrawerWorkflowSelectTriggerTypeContent = ({
|
export const RightDrawerWorkflowSelectTriggerTypeContent = ({
|
||||||
workflow,
|
workflow,
|
||||||
@ -26,6 +29,33 @@ export const RightDrawerWorkflowSelectTriggerTypeContent = ({
|
|||||||
const { openRightDrawer } = useRightDrawer();
|
const { openRightDrawer } = useRightDrawer();
|
||||||
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
||||||
|
|
||||||
|
const handleTriggerTypeClick = ({
|
||||||
|
type,
|
||||||
|
defaultLabel,
|
||||||
|
icon,
|
||||||
|
}: {
|
||||||
|
type: WorkflowTriggerType;
|
||||||
|
defaultLabel: string;
|
||||||
|
icon: string;
|
||||||
|
}) => {
|
||||||
|
return async () => {
|
||||||
|
await updateTrigger(
|
||||||
|
getTriggerDefaultDefinition({
|
||||||
|
defaultLabel,
|
||||||
|
type,
|
||||||
|
activeObjectMetadataItems,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
setWorkflowSelectedNode(TRIGGER_STEP_ID);
|
||||||
|
|
||||||
|
openRightDrawer(RightDrawerPages.WorkflowStepEdit, {
|
||||||
|
title: defaultLabel,
|
||||||
|
Icon: getIcon(icon),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RightDrawerStepListContainer>
|
<RightDrawerStepListContainer>
|
||||||
<RightDrawerWorkflowSelectStepTitle>
|
<RightDrawerWorkflowSelectStepTitle>
|
||||||
@ -36,22 +66,7 @@ export const RightDrawerWorkflowSelectTriggerTypeContent = ({
|
|||||||
key={action.defaultLabel}
|
key={action.defaultLabel}
|
||||||
LeftIcon={getIcon(action.icon)}
|
LeftIcon={getIcon(action.icon)}
|
||||||
text={action.defaultLabel}
|
text={action.defaultLabel}
|
||||||
onClick={async () => {
|
onClick={handleTriggerTypeClick(action)}
|
||||||
await updateTrigger(
|
|
||||||
getTriggerDefaultDefinition({
|
|
||||||
defaultLabel: action.defaultLabel,
|
|
||||||
type: action.type,
|
|
||||||
activeObjectMetadataItems,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
setWorkflowSelectedNode(TRIGGER_STEP_ID);
|
|
||||||
|
|
||||||
openRightDrawer(RightDrawerPages.WorkflowStepEdit, {
|
|
||||||
title: action.defaultLabel,
|
|
||||||
Icon: getIcon(action.icon),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<RightDrawerWorkflowSelectStepTitle>
|
<RightDrawerWorkflowSelectStepTitle>
|
||||||
@ -62,22 +77,7 @@ export const RightDrawerWorkflowSelectTriggerTypeContent = ({
|
|||||||
key={action.defaultLabel}
|
key={action.defaultLabel}
|
||||||
LeftIcon={getIcon(action.icon)}
|
LeftIcon={getIcon(action.icon)}
|
||||||
text={action.defaultLabel}
|
text={action.defaultLabel}
|
||||||
onClick={async () => {
|
onClick={handleTriggerTypeClick(action)}
|
||||||
await updateTrigger(
|
|
||||||
getTriggerDefaultDefinition({
|
|
||||||
defaultLabel: action.defaultLabel,
|
|
||||||
type: action.type,
|
|
||||||
activeObjectMetadataItems,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
setWorkflowSelectedNode(TRIGGER_STEP_ID);
|
|
||||||
|
|
||||||
openRightDrawer(RightDrawerPages.WorkflowStepEdit, {
|
|
||||||
title: action.defaultLabel,
|
|
||||||
Icon: getIcon(action.icon),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</RightDrawerStepListContainer>
|
</RightDrawerStepListContainer>
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import {
|
|||||||
getWorkflowVersionsMock,
|
getWorkflowVersionsMock,
|
||||||
workflowQueryResult,
|
workflowQueryResult,
|
||||||
} from '~/testing/mock-data/workflow';
|
} from '~/testing/mock-data/workflow';
|
||||||
|
import { oneSucceededWorkflowRunQueryResult } from '~/testing/mock-data/workflow-run';
|
||||||
import { mockedRemoteServers } from './mock-data/remote-servers';
|
import { mockedRemoteServers } from './mock-data/remote-servers';
|
||||||
import { mockedViewFieldsData } from './mock-data/view-fields';
|
import { mockedViewFieldsData } from './mock-data/view-fields';
|
||||||
|
|
||||||
@ -714,6 +715,11 @@ export const graphqlMocks = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
graphql.query('FindOneWorkflowRun', () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
data: oneSucceededWorkflowRunQueryResult,
|
||||||
|
});
|
||||||
|
}),
|
||||||
graphql.query('FindManyWorkflowVersions', () => {
|
graphql.query('FindManyWorkflowVersions', () => {
|
||||||
return HttpResponse.json({
|
return HttpResponse.json({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
12009
packages/twenty-front/src/testing/mock-data/workflow-run.ts
Normal file
12009
packages/twenty-front/src/testing/mock-data/workflow-run.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user