diff --git a/packages/twenty-e2e-testing/tests/workflow-run.spec.ts b/packages/twenty-e2e-testing/tests/workflow-run.spec.ts
new file mode 100644
index 000000000..2ba3341d5
--- /dev/null
+++ b/packages/twenty-e2e-testing/tests/workflow-run.spec.ts
@@ -0,0 +1,60 @@
+import { expect } from '@playwright/test';
+import { test } from '../lib/fixtures/blank-workflow';
+
+test('The workflow run visualizer shows the executed draft version without the last draft changes', async ({
+ workflowVisualizer,
+ page,
+}) => {
+ await workflowVisualizer.createInitialTrigger('manual');
+
+ const manualTriggerAvailabilitySelect = page.getByRole('button', {
+ name: 'When record(s) are selected',
+ });
+
+ await manualTriggerAvailabilitySelect.click();
+
+ const alwaysAvailableOption = page.getByText(
+ 'When no record(s) are selected',
+ );
+
+ await alwaysAvailableOption.click();
+
+ await workflowVisualizer.closeSidePanel();
+
+ const { createdStepId: firstStepId } =
+ await workflowVisualizer.createStep('create-record');
+
+ await workflowVisualizer.closeSidePanel();
+
+ const launchTestButton = page.getByRole('button', { name: 'Test' });
+
+ await launchTestButton.click();
+
+ const goToExecutionPageLink = page.getByRole('link', {
+ name: 'View execution details',
+ });
+ const executionPageUrl = await goToExecutionPageLink.getAttribute('href');
+ expect(executionPageUrl).not.toBeNull();
+
+ await workflowVisualizer.deleteStep(firstStepId);
+
+ await page.goto(executionPageUrl!);
+
+ const workflowRunName = page.getByText('Execution of v1');
+
+ await expect(workflowRunName).toBeVisible();
+
+ const flowTab = page.getByText('Flow', { exact: true });
+
+ await flowTab.click();
+
+ const executedFirstStepNode = workflowVisualizer.getStepNode(firstStepId);
+
+ await expect(executedFirstStepNode).toBeVisible();
+
+ await executedFirstStepNode.click();
+
+ await expect(
+ workflowVisualizer.commandMenu.getByRole('textbox').first(),
+ ).toHaveValue('Create Record');
+});
diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowRunVisualizerContent.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowRunVisualizerContent.tsx
index 903234264..49d4e58bf 100644
--- a/packages/twenty-front/src/modules/workflow/components/WorkflowRunVisualizerContent.tsx
+++ b/packages/twenty-front/src/modules/workflow/components/WorkflowRunVisualizerContent.tsx
@@ -16,10 +16,7 @@ export const WorkflowRunVisualizerContent = ({
return (
<>
-
+
>
diff --git a/packages/twenty-front/src/modules/workflow/hooks/useFlowOrThrow.ts b/packages/twenty-front/src/modules/workflow/hooks/useFlowOrThrow.ts
new file mode 100644
index 000000000..99fd038a8
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/hooks/useFlowOrThrow.ts
@@ -0,0 +1,12 @@
+import { flowState } from '@/workflow/states/flowState';
+import { useRecoilValue } from 'recoil';
+import { isDefined } from 'twenty-shared';
+
+export const useFlowOrThrow = () => {
+ const flow = useRecoilValue(flowState);
+ if (!isDefined(flow)) {
+ throw new Error('Expected the flow to be defined');
+ }
+
+ return flow;
+};
diff --git a/packages/twenty-front/src/modules/workflow/states/flowState.ts b/packages/twenty-front/src/modules/workflow/states/flowState.ts
new file mode 100644
index 000000000..21216f77a
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/states/flowState.ts
@@ -0,0 +1,13 @@
+import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow';
+import { createState } from '@ui/utilities/state/utils/createState';
+
+export const flowState = createState<
+ | {
+ trigger: WorkflowTrigger | null;
+ steps: WorkflowAction[] | null;
+ }
+ | undefined
+>({
+ key: 'flowState',
+ defaultValue: undefined,
+});
diff --git a/packages/twenty-front/src/modules/workflow/states/workflowVersionIdState.ts b/packages/twenty-front/src/modules/workflow/states/workflowVersionIdState.ts
deleted file mode 100644
index f72cb3933..000000000
--- a/packages/twenty-front/src/modules/workflow/states/workflowVersionIdState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { createState } from '@ui/utilities/state/utils/createState';
-
-export const workflowVersionIdState = createState({
- key: 'workflowVersionIdState',
- defaultValue: undefined,
-});
diff --git a/packages/twenty-front/src/modules/workflow/utils/getStepDefinitionOrThrow.ts b/packages/twenty-front/src/modules/workflow/utils/getStepDefinitionOrThrow.ts
index 74fe0bbea..6aa897936 100644
--- a/packages/twenty-front/src/modules/workflow/utils/getStepDefinitionOrThrow.ts
+++ b/packages/twenty-front/src/modules/workflow/utils/getStepDefinitionOrThrow.ts
@@ -1,17 +1,19 @@
-import { WorkflowVersion } from '@/workflow/types/Workflow';
+import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow';
import { findStepPosition } from '@/workflow/utils/findStepPosition';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import { isDefined } from 'twenty-shared';
export const getStepDefinitionOrThrow = ({
stepId,
- workflowVersion,
+ trigger,
+ steps,
}: {
stepId: string;
- workflowVersion: WorkflowVersion;
+ trigger: WorkflowTrigger | null;
+ steps: Array | null;
}) => {
if (stepId === TRIGGER_STEP_ID) {
- if (!isDefined(workflowVersion.trigger)) {
+ if (!isDefined(trigger)) {
return {
type: 'trigger',
definition: undefined,
@@ -20,18 +22,18 @@ export const getStepDefinitionOrThrow = ({
return {
type: 'trigger',
- definition: workflowVersion.trigger,
+ definition: trigger,
} as const;
}
- if (!isDefined(workflowVersion.steps)) {
+ if (!isDefined(steps)) {
throw new Error(
'Malformed workflow version: missing steps information; be sure to create at least one step before trying to edit one',
);
}
const selectedNodePosition = findStepPosition({
- steps: workflowVersion.steps,
+ steps,
stepId: stepId,
});
if (!isDefined(selectedNodePosition)) {
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx
index a85807c22..f781581fe 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx
@@ -1,4 +1,5 @@
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
+import { flowState } from '@/workflow/states/flowState';
import { workflowLastCreatedStepIdState } from '@/workflow/states/workflowLastCreatedStepIdState';
import {
WorkflowVersion,
@@ -20,6 +21,7 @@ export const WorkflowDiagramEffect = ({
workflowWithCurrentVersion: WorkflowWithCurrentVersion | undefined;
}) => {
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
+ const setFlow = useSetRecoilState(flowState);
const computeAndMergeNewWorkflowDiagram = useRecoilCallback(
({ snapshot, set }) => {
@@ -67,14 +69,21 @@ export const WorkflowDiagramEffect = ({
useEffect(() => {
const currentVersion = workflowWithCurrentVersion?.currentVersion;
if (!isDefined(currentVersion)) {
+ setFlow(undefined);
setWorkflowDiagram(undefined);
return;
}
+ setFlow({
+ trigger: currentVersion.trigger,
+ steps: currentVersion.steps,
+ });
+
computeAndMergeNewWorkflowDiagram(currentVersion);
}, [
computeAndMergeNewWorkflowDiagram,
+ setFlow,
setWorkflowDiagram,
workflowWithCurrentVersion?.currentVersion,
]);
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx
index 23f3be1e9..f105b0493 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx
@@ -1,4 +1,4 @@
-import { workflowVersionIdState } from '@/workflow/states/workflowVersionIdState';
+import { flowState } from '@/workflow/states/flowState';
import { WorkflowRun } from '@/workflow/types/Workflow';
import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflowDiagramState';
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
@@ -7,18 +7,25 @@ import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
export const WorkflowRunVisualizerEffect = ({
- workflowVersionId,
workflowRun,
}: {
- workflowVersionId: string;
workflowRun: WorkflowRun;
}) => {
- const setWorkflowVersionId = useSetRecoilState(workflowVersionIdState);
+ const setFlow = useSetRecoilState(flowState);
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
useEffect(() => {
- setWorkflowVersionId(workflowVersionId);
- }, [setWorkflowVersionId, workflowVersionId]);
+ if (!isDefined(workflowRun.output)) {
+ setFlow(undefined);
+
+ return;
+ }
+
+ setFlow({
+ trigger: workflowRun.output.flow.trigger,
+ steps: workflowRun.output.flow.steps,
+ });
+ }, [setFlow, workflowRun.output]);
useEffect(() => {
if (!isDefined(workflowRun.output)) {
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx
index 3d5a3e4c1..47c0024fe 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx
@@ -1,5 +1,5 @@
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
-import { workflowVersionIdState } from '@/workflow/states/workflowVersionIdState';
+import { flowState } from '@/workflow/states/flowState';
import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflowDiagramState';
import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram';
import { markLeafNodes } from '@/workflow/workflow-diagram/utils/markLeafNodes';
@@ -14,12 +14,21 @@ export const WorkflowVersionVisualizerEffect = ({
}) => {
const workflowVersion = useWorkflowVersion(workflowVersionId);
- const setWorkflowVersionId = useSetRecoilState(workflowVersionIdState);
+ const setFlow = useSetRecoilState(flowState);
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
useEffect(() => {
- setWorkflowVersionId(workflowVersionId);
- }, [setWorkflowVersionId, workflowVersionId]);
+ if (!isDefined(workflowVersion)) {
+ setFlow(undefined);
+
+ return;
+ }
+
+ setFlow({
+ trigger: workflowVersion.trigger,
+ steps: workflowVersion.steps,
+ });
+ }, [setFlow, workflowVersion]);
useEffect(() => {
if (!isDefined(workflowVersion)) {
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowEditStepContent.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowEditStepContent.tsx
index 37aadc98d..4dfd8183b 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowEditStepContent.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowEditStepContent.tsx
@@ -1,3 +1,4 @@
+import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
@@ -11,6 +12,8 @@ export const RightDrawerWorkflowEditStepContent = ({
}: {
workflow: WorkflowWithCurrentVersion;
}) => {
+ const flow = useFlowOrThrow();
+
const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
if (!isDefined(workflowSelectedNode)) {
throw new Error(
@@ -26,7 +29,8 @@ export const RightDrawerWorkflowEditStepContent = ({
return (
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowViewStep.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowViewStep.tsx
index c1f6540cb..a029adf75 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowViewStep.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowViewStep.tsx
@@ -1,22 +1,25 @@
-import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
-import { workflowVersionIdState } from '@/workflow/states/workflowVersionIdState';
-import { RightDrawerWorkflowViewStepContent } from '@/workflow/workflow-steps/components/RightDrawerWorkflowViewStepContent';
+import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
+import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
+import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
export const RightDrawerWorkflowViewStep = () => {
- const workflowVersionId = useRecoilValue(workflowVersionIdState);
- if (!isDefined(workflowVersionId)) {
- throw new Error('Expected a workflow version id');
- }
+ const flow = useFlowOrThrow();
- const workflowVersion = useWorkflowVersion(workflowVersionId);
-
- if (!isDefined(workflowVersion)) {
- return null;
+ const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
+ if (!isDefined(workflowSelectedNode)) {
+ throw new Error(
+ 'Expected a node to be selected. Selecting a node is mandatory to view its details.',
+ );
}
return (
-
+
);
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowViewStepContent.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowViewStepContent.tsx
deleted file mode 100644
index c7a635664..000000000
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowViewStepContent.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { WorkflowVersion } from '@/workflow/types/Workflow';
-import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
-import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
-import { useRecoilValue } from 'recoil';
-import { isDefined } from 'twenty-shared';
-
-export const RightDrawerWorkflowViewStepContent = ({
- workflowVersion,
-}: {
- workflowVersion: WorkflowVersion;
-}) => {
- const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
- if (!isDefined(workflowSelectedNode)) {
- throw new Error(
- 'Expected a node to be selected. Selecting a node is mandatory to edit it.',
- );
- }
-
- return (
-
- );
-};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx
index 373570359..ed9b8af02 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx
@@ -1,8 +1,4 @@
-import {
- WorkflowAction,
- WorkflowTrigger,
- WorkflowVersion,
-} from '@/workflow/types/Workflow';
+import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
import { WorkflowEditActionFormCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormCreateRecord';
@@ -25,30 +21,34 @@ const WorkflowEditActionFormServerlessFunction = lazy(() =>
})),
);
-type WorkflowStepDetailProps =
+type WorkflowStepDetailProps = {
+ stepId: string;
+ trigger: WorkflowTrigger | null;
+ steps: Array | null;
+} & (
| {
- stepId: string;
- workflowVersion: WorkflowVersion;
readonly: true;
onTriggerUpdate?: undefined;
onActionUpdate?: undefined;
}
| {
stepId: string;
- workflowVersion: WorkflowVersion;
readonly?: false;
onTriggerUpdate: (trigger: WorkflowTrigger) => void;
onActionUpdate: (action: WorkflowAction) => void;
- };
+ }
+);
export const WorkflowStepDetail = ({
stepId,
- workflowVersion,
+ trigger,
+ steps,
...props
}: WorkflowStepDetailProps) => {
const stepDefinition = getStepDefinitionOrThrow({
stepId,
- workflowVersion,
+ trigger,
+ steps,
});
if (!isDefined(stepDefinition) || !isDefined(stepDefinition.definition)) {
return null;
diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts
index 233cfa25c..9a1de459e 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts
@@ -1,3 +1,4 @@
+import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
@@ -24,27 +25,32 @@ export const useAvailableVariablesInWorkflowStep = ({
const workflowId = useRecoilValue(workflowIdState);
const workflow = useWorkflowWithCurrentVersion(workflowId);
const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
+ const flow = useFlowOrThrow();
if (!isDefined(workflowSelectedNode) || !isDefined(workflow)) {
return [];
}
+ const trigger = flow.trigger;
+ const steps = flow.steps;
+
const stepDefinition = getStepDefinitionOrThrow({
stepId: workflowSelectedNode,
- workflowVersion: workflow.currentVersion,
+ trigger,
+ steps,
});
if (
!isDefined(stepDefinition) ||
stepDefinition.type === 'trigger' ||
- !isDefined(workflow.currentVersion.steps)
+ !isDefined(steps)
) {
return [];
}
const previousSteps = [];
- for (const step of workflow.currentVersion.steps) {
+ for (const step of steps) {
if (step.id === workflowSelectedNode) {
break;
}
@@ -54,34 +60,32 @@ export const useAvailableVariablesInWorkflowStep = ({
const result = [];
const filteredTriggerOutputSchema = filterOutputSchema(
- workflow.currentVersion.trigger?.settings?.outputSchema as
- | OutputSchema
- | undefined,
+ trigger?.settings?.outputSchema as OutputSchema | undefined,
objectNameSingularToSelect,
);
if (
- isDefined(workflow.currentVersion.trigger) &&
+ isDefined(trigger) &&
isDefined(filteredTriggerOutputSchema) &&
!isEmptyObject(filteredTriggerOutputSchema)
) {
const triggerIconKey =
- workflow.currentVersion.trigger.type === 'DATABASE_EVENT'
+ trigger.type === 'DATABASE_EVENT'
? getTriggerIcon({
- type: workflow.currentVersion.trigger.type,
+ type: trigger.type,
eventName: splitWorkflowTriggerEventName(
- workflow.currentVersion.trigger.settings?.eventName,
+ trigger.settings?.eventName,
).event,
})
: getTriggerIcon({
- type: workflow.currentVersion.trigger.type,
+ type: trigger.type,
});
result.push({
id: 'trigger',
- name: isDefined(workflow.currentVersion.trigger.name)
- ? workflow.currentVersion.trigger.name
- : getTriggerStepName(workflow.currentVersion.trigger),
+ name: isDefined(trigger.name)
+ ? trigger.name
+ : getTriggerStepName(trigger),
icon: triggerIconKey,
outputSchema: filteredTriggerOutputSchema,
});