Make filters a regular step (#14586)
- Remove special design logic for filters and edge with filters - Remove backend logic to delete filters along with parent steps - Add filters to the node type picker https://github.com/user-attachments/assets/5b6ceda4-d86f-48a3-8c93-37a885398a8d
This commit is contained in:
+11
-13
@@ -4,8 +4,8 @@ import { RightDrawerStepListContainer } from '@/workflow/workflow-steps/componen
|
||||
import { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepTitle';
|
||||
import { AI_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/AiActions';
|
||||
import { CORE_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/CoreActions';
|
||||
import { FLOW_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/FlowActions';
|
||||
import { HUMAN_INPUT_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/HumanInputActions';
|
||||
import { OTHER_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/OtherActions';
|
||||
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
@@ -45,6 +45,16 @@ export const CommandMenuWorkflowSelectAction = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
<RightDrawerWorkflowSelectStepTitle>
|
||||
{t`Flow`}
|
||||
</RightDrawerWorkflowSelectStepTitle>
|
||||
<WorkflowActionMenuItems
|
||||
actions={FLOW_ACTIONS.filter(
|
||||
(action) => action.type !== 'ITERATOR' || isIteratorEnabled,
|
||||
)}
|
||||
onClick={onActionSelected}
|
||||
/>
|
||||
|
||||
<RightDrawerWorkflowSelectStepTitle>
|
||||
{t`Core`}
|
||||
</RightDrawerWorkflowSelectStepTitle>
|
||||
@@ -60,18 +70,6 @@ export const CommandMenuWorkflowSelectAction = ({
|
||||
actions={HUMAN_INPUT_ACTIONS}
|
||||
onClick={onActionSelected}
|
||||
/>
|
||||
|
||||
{isIteratorEnabled && (
|
||||
<>
|
||||
<RightDrawerWorkflowSelectStepTitle>
|
||||
{t`Others`}
|
||||
</RightDrawerWorkflowSelectStepTitle>
|
||||
<WorkflowActionMenuItems
|
||||
actions={OTHER_ACTIONS}
|
||||
onClick={onActionSelected}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</RightDrawerStepListContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-3
@@ -14,7 +14,6 @@ import {
|
||||
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
|
||||
import { WorkflowDiagramBlankEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBlankEdge';
|
||||
import { WorkflowDiagramDefaultEdgeEditable } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeEditable';
|
||||
import { WorkflowDiagramFilterEdgeEditable } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable';
|
||||
import { WorkflowDiagramEmptyTriggerEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerEditable';
|
||||
import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditable';
|
||||
import { useCreateEdge } from '@/workflow/workflow-steps/hooks/useCreateEdge';
|
||||
@@ -128,8 +127,7 @@ export const WorkflowDiagramCanvasEditable = () => {
|
||||
}}
|
||||
edgeTypes={{
|
||||
blank: WorkflowDiagramBlankEdge,
|
||||
'empty-filter--editable': WorkflowDiagramDefaultEdgeEditable,
|
||||
'filter--editable': WorkflowDiagramFilterEdgeEditable,
|
||||
editable: WorkflowDiagramDefaultEdgeEditable,
|
||||
}}
|
||||
tagContainerTestId="workflow-visualizer-status"
|
||||
tagColor={tagProps.color}
|
||||
|
||||
+1
-3
@@ -4,7 +4,6 @@ import { WorkflowDiagramDefaultEdgeReadonly } from '@/workflow/workflow-diagram/
|
||||
|
||||
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
|
||||
import { WorkflowDiagramEmptyTriggerReadonly } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerReadonly';
|
||||
import { WorkflowDiagramFilterEdgeReadonly } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeReadonly';
|
||||
import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeReadonly';
|
||||
import { ReactFlowProvider } from '@xyflow/react';
|
||||
|
||||
@@ -25,8 +24,7 @@ export const WorkflowDiagramCanvasReadonly = ({
|
||||
'empty-trigger': WorkflowDiagramEmptyTriggerReadonly,
|
||||
}}
|
||||
edgeTypes={{
|
||||
'empty-filter--readonly': WorkflowDiagramDefaultEdgeReadonly,
|
||||
'filter--readonly': WorkflowDiagramFilterEdgeReadonly,
|
||||
readonly: WorkflowDiagramDefaultEdgeReadonly,
|
||||
}}
|
||||
tagContainerTestId="workflow-visualizer-status"
|
||||
tagColor={tagProps.color}
|
||||
|
||||
+1
-3
@@ -1,7 +1,6 @@
|
||||
import { type WorkflowRunStatus } from '@/workflow/types/Workflow';
|
||||
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
|
||||
import { WorkflowDiagramDefaultEdgeRun } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeRun';
|
||||
import { WorkflowDiagramFilterEdgeRun } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeRun';
|
||||
|
||||
import { getWorkflowRunStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps';
|
||||
import { WorkflowRunDiagramStepNode } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowRunDiagramStepNode';
|
||||
@@ -23,8 +22,7 @@ export const WorkflowRunDiagramCanvas = ({
|
||||
default: WorkflowRunDiagramStepNode,
|
||||
}}
|
||||
edgeTypes={{
|
||||
'empty-filter--run': WorkflowDiagramDefaultEdgeRun,
|
||||
'filter--run': WorkflowDiagramFilterEdgeRun,
|
||||
run: WorkflowDiagramDefaultEdgeRun,
|
||||
}}
|
||||
tagContainerTestId="workflow-run-status"
|
||||
tagColor={tagProps.color}
|
||||
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
export const useOpenWorkflowEditFilterInCommandMenu = () => {
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const workflowVisualizerWorkflowId = useRecoilComponentValue(
|
||||
workflowVisualizerWorkflowIdComponentState,
|
||||
);
|
||||
const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu();
|
||||
|
||||
const setWorkflowSelectedNode = useSetRecoilComponentState(
|
||||
workflowSelectedNodeComponentState,
|
||||
);
|
||||
const setWorkflowDiagram = useSetRecoilComponentState(
|
||||
workflowDiagramComponentState,
|
||||
);
|
||||
|
||||
const openWorkflowEditFilterInCommandMenu = ({
|
||||
stepId,
|
||||
stepName,
|
||||
}: {
|
||||
stepId: string;
|
||||
stepName: string;
|
||||
}) => {
|
||||
if (!isDefined(workflowVisualizerWorkflowId)) {
|
||||
throw new Error(
|
||||
'Workflow ID must be configured for the edge when opening a filter in command menu',
|
||||
);
|
||||
}
|
||||
|
||||
setWorkflowSelectedNode(stepId);
|
||||
|
||||
setWorkflowDiagram((diagram) => {
|
||||
if (!isDefined(diagram)) {
|
||||
throw new Error('Workflow diagram must be defined');
|
||||
}
|
||||
|
||||
return {
|
||||
...diagram,
|
||||
nodes: diagram.nodes.map((node) => ({
|
||||
...node,
|
||||
selected: node.id === stepId,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
openWorkflowEditStepInCommandMenu(
|
||||
workflowVisualizerWorkflowId,
|
||||
stepName,
|
||||
getIcon(getActionIcon('FILTER')),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
openWorkflowEditFilterInCommandMenu,
|
||||
};
|
||||
};
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { workflowVisualizerWorkflowRunIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowRunIdComponentState';
|
||||
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type StepStatus } from 'twenty-shared/workflow';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
export const useOpenWorkflowRunFilterInCommandMenu = () => {
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const workflowVisualizerWorkflowId = useRecoilComponentValue(
|
||||
workflowVisualizerWorkflowIdComponentState,
|
||||
);
|
||||
const workflowVisualizerWorkflowRunId = useRecoilComponentValue(
|
||||
workflowVisualizerWorkflowRunIdComponentState,
|
||||
);
|
||||
|
||||
const setWorkflowSelectedNode = useSetRecoilComponentState(
|
||||
workflowSelectedNodeComponentState,
|
||||
);
|
||||
const setWorkflowDiagram = useSetRecoilComponentState(
|
||||
workflowDiagramComponentState,
|
||||
);
|
||||
|
||||
const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu();
|
||||
|
||||
const openWorkflowRunFilterInCommandMenu = ({
|
||||
stepId,
|
||||
stepName,
|
||||
stepExecutionStatus,
|
||||
}: {
|
||||
stepId: string;
|
||||
stepName: string;
|
||||
stepExecutionStatus: StepStatus;
|
||||
}) => {
|
||||
if (!isDefined(workflowVisualizerWorkflowId)) {
|
||||
throw new Error('Workflow ID is required');
|
||||
}
|
||||
|
||||
if (!isDefined(workflowVisualizerWorkflowRunId)) {
|
||||
throw new Error('Workflow run ID is required');
|
||||
}
|
||||
|
||||
setWorkflowSelectedNode(stepId);
|
||||
|
||||
setWorkflowDiagram((diagram) => {
|
||||
if (!isDefined(diagram)) {
|
||||
throw new Error('Workflow diagram must be defined');
|
||||
}
|
||||
|
||||
return {
|
||||
...diagram,
|
||||
nodes: diagram.nodes.map((node) => ({
|
||||
...node,
|
||||
selected: false,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
openWorkflowRunViewStepInCommandMenu({
|
||||
workflowId: workflowVisualizerWorkflowId,
|
||||
workflowRunId: workflowVisualizerWorkflowRunId,
|
||||
title: stepName,
|
||||
icon: getIcon(getActionIcon('FILTER')),
|
||||
workflowSelectedNode: stepId,
|
||||
stepExecutionStatus,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
openWorkflowRunFilterInCommandMenu,
|
||||
};
|
||||
};
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState';
|
||||
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
export const useOpenWorkflowViewFilterInCommandMenu = () => {
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const workflowVisualizerWorkflowId = useRecoilComponentValue(
|
||||
workflowVisualizerWorkflowIdComponentState,
|
||||
);
|
||||
const workflowVisualizerWorkflowVersionId = useRecoilComponentValue(
|
||||
workflowVisualizerWorkflowVersionIdComponentState,
|
||||
);
|
||||
const { openWorkflowViewStepInCommandMenu } = useWorkflowCommandMenu();
|
||||
|
||||
const setWorkflowSelectedNode = useSetRecoilComponentState(
|
||||
workflowSelectedNodeComponentState,
|
||||
);
|
||||
const setWorkflowDiagram = useSetRecoilComponentState(
|
||||
workflowDiagramComponentState,
|
||||
);
|
||||
|
||||
const openWorkflowViewFilterInCommandMenu = ({
|
||||
stepId,
|
||||
stepName,
|
||||
}: {
|
||||
stepId: string;
|
||||
stepName: string;
|
||||
}) => {
|
||||
if (!workflowVisualizerWorkflowId) {
|
||||
throw new Error('Workflow ID is required');
|
||||
}
|
||||
|
||||
if (!workflowVisualizerWorkflowVersionId) {
|
||||
throw new Error('Workflow version ID is required');
|
||||
}
|
||||
|
||||
setWorkflowSelectedNode(stepId);
|
||||
|
||||
setWorkflowDiagram((diagram) => {
|
||||
if (!isDefined(diagram)) {
|
||||
throw new Error('Workflow diagram must be defined');
|
||||
}
|
||||
|
||||
return {
|
||||
...diagram,
|
||||
nodes: diagram.nodes.map((node) => ({
|
||||
...node,
|
||||
selected: false,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
openWorkflowViewStepInCommandMenu({
|
||||
workflowId: workflowVisualizerWorkflowId,
|
||||
workflowVersionId: workflowVisualizerWorkflowVersionId,
|
||||
title: stepName,
|
||||
icon: getIcon(getActionIcon('FILTER')),
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
openWorkflowViewFilterInCommandMenu,
|
||||
};
|
||||
};
|
||||
+2
-22
@@ -3,7 +3,6 @@ import {
|
||||
type WorkflowRunStepStatus,
|
||||
type WorkflowTriggerType,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { type FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import {
|
||||
type Connection,
|
||||
@@ -105,33 +104,14 @@ export type WorkflowDiagramEdgeLabelOptions = {
|
||||
label: MessageDescriptor;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramFilterEdgeData = {
|
||||
edgeType: 'filter';
|
||||
stepId: string;
|
||||
filterSettings: FilterSettings;
|
||||
name: string;
|
||||
runStatus?: WorkflowRunStepStatus;
|
||||
edgeExecutionStatus?: StepStatus;
|
||||
labelOptions?: WorkflowDiagramEdgeLabelOptions;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramDefaultEdgeData = {
|
||||
edgeType: 'default';
|
||||
edgeExecutionStatus?: StepStatus;
|
||||
labelOptions?: WorkflowDiagramEdgeLabelOptions;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramEdgeData =
|
||||
| WorkflowDiagramFilterEdgeData
|
||||
| WorkflowDiagramDefaultEdgeData;
|
||||
export type WorkflowDiagramEdgeData = WorkflowDiagramDefaultEdgeData;
|
||||
|
||||
export type WorkflowDiagramNodeType = 'default' | 'empty-trigger';
|
||||
|
||||
export type WorkflowDiagramEdgeType =
|
||||
| 'blank'
|
||||
| 'empty-filter--editable'
|
||||
| 'empty-filter--readonly'
|
||||
| 'empty-filter--run'
|
||||
| 'filter--editable'
|
||||
| 'filter--readonly'
|
||||
| 'filter--run';
|
||||
export type WorkflowDiagramEdgeType = 'blank' | 'editable' | 'readonly' | 'run';
|
||||
|
||||
+14
-14
@@ -124,7 +124,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"sourceHandle": "default",
|
||||
"target": "step1",
|
||||
"targetHandle": "default",
|
||||
"type": "empty-filter--run",
|
||||
"type": "run",
|
||||
"zIndex": -2,
|
||||
},
|
||||
{
|
||||
@@ -141,7 +141,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"sourceHandle": "default",
|
||||
"target": "step2",
|
||||
"targetHandle": "default",
|
||||
"type": "empty-filter--run",
|
||||
"type": "run",
|
||||
"zIndex": -2,
|
||||
},
|
||||
{
|
||||
@@ -158,7 +158,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"sourceHandle": "default",
|
||||
"target": "step3",
|
||||
"targetHandle": "default",
|
||||
"type": "empty-filter--run",
|
||||
"type": "run",
|
||||
"zIndex": -2,
|
||||
},
|
||||
],
|
||||
@@ -361,7 +361,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"sourceHandle": "default",
|
||||
"target": "step1",
|
||||
"targetHandle": "default",
|
||||
"type": "empty-filter--run",
|
||||
"type": "run",
|
||||
"zIndex": -2,
|
||||
},
|
||||
{
|
||||
@@ -378,7 +378,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"sourceHandle": "default",
|
||||
"target": "step2",
|
||||
"targetHandle": "default",
|
||||
"type": "empty-filter--run",
|
||||
"type": "run",
|
||||
"zIndex": -2,
|
||||
},
|
||||
{
|
||||
@@ -395,7 +395,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"sourceHandle": "default",
|
||||
"target": "step3",
|
||||
"targetHandle": "default",
|
||||
"type": "empty-filter--run",
|
||||
"type": "run",
|
||||
"zIndex": -2,
|
||||
},
|
||||
],
|
||||
@@ -598,7 +598,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"sourceHandle": "default",
|
||||
"target": "step1",
|
||||
"targetHandle": "default",
|
||||
"type": "empty-filter--run",
|
||||
"type": "run",
|
||||
"zIndex": -2,
|
||||
},
|
||||
{
|
||||
@@ -615,7 +615,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"sourceHandle": "default",
|
||||
"target": "step2",
|
||||
"targetHandle": "default",
|
||||
"type": "empty-filter--run",
|
||||
"type": "run",
|
||||
"zIndex": -2,
|
||||
},
|
||||
{
|
||||
@@ -632,7 +632,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"sourceHandle": "default",
|
||||
"target": "step3",
|
||||
"targetHandle": "default",
|
||||
"type": "empty-filter--run",
|
||||
"type": "run",
|
||||
"zIndex": -2,
|
||||
},
|
||||
],
|
||||
@@ -854,7 +854,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"sourceHandle": "default",
|
||||
"target": "step1",
|
||||
"targetHandle": "default",
|
||||
"type": "empty-filter--run",
|
||||
"type": "run",
|
||||
"zIndex": -2,
|
||||
},
|
||||
{
|
||||
@@ -871,7 +871,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"sourceHandle": "default",
|
||||
"target": "step2",
|
||||
"targetHandle": "default",
|
||||
"type": "empty-filter--run",
|
||||
"type": "run",
|
||||
"zIndex": -2,
|
||||
},
|
||||
{
|
||||
@@ -888,7 +888,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"sourceHandle": "default",
|
||||
"target": "step3",
|
||||
"targetHandle": "default",
|
||||
"type": "empty-filter--run",
|
||||
"type": "run",
|
||||
"zIndex": -2,
|
||||
},
|
||||
{
|
||||
@@ -905,7 +905,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"sourceHandle": "default",
|
||||
"target": "step4",
|
||||
"targetHandle": "default",
|
||||
"type": "empty-filter--run",
|
||||
"type": "run",
|
||||
"zIndex": -2,
|
||||
},
|
||||
],
|
||||
@@ -1093,7 +1093,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"sourceHandle": "default",
|
||||
"target": "step1",
|
||||
"targetHandle": "default",
|
||||
"type": "empty-filter--run",
|
||||
"type": "run",
|
||||
"zIndex": -2,
|
||||
},
|
||||
],
|
||||
|
||||
+1
-1
@@ -164,7 +164,7 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
"sourceHandle": "default",
|
||||
"target": "step-1",
|
||||
"targetHandle": "default",
|
||||
"type": "empty-filter--editable",
|
||||
"type": "editable",
|
||||
"zIndex": -2,
|
||||
},
|
||||
],
|
||||
|
||||
-511
@@ -1,511 +0,0 @@
|
||||
import { type WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { transformFilterNodesAsEdges } from '../transformFilterNodesAsEdges';
|
||||
|
||||
describe('transformFilterNodesAsEdges', () => {
|
||||
it('should return the original diagram when there are no filter nodes', () => {
|
||||
const diagram: WorkflowDiagram = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: 'Step A',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'A',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Step C',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'C',
|
||||
},
|
||||
position: { x: 0, y: 300 },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'A-C',
|
||||
source: 'A',
|
||||
sourceHandle: 'default',
|
||||
target: 'C',
|
||||
targetHandle: 'default',
|
||||
data: {
|
||||
edgeType: 'default',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = transformFilterNodesAsEdges({
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
expect(result.nodes).toEqual(diagram.nodes);
|
||||
expect(result.edges).toEqual(diagram.edges);
|
||||
});
|
||||
|
||||
it('should transform A->B->C where B is a FILTER step', () => {
|
||||
const diagram: WorkflowDiagram = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: 'Step A',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'A',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'FILTER',
|
||||
name: 'Filter B',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 150 },
|
||||
stepId: 'B',
|
||||
},
|
||||
position: { x: 0, y: 150 },
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Step C',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 300 },
|
||||
stepId: 'C',
|
||||
},
|
||||
position: { x: 0, y: 300 },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'A-B',
|
||||
source: 'A',
|
||||
sourceHandle: 'default',
|
||||
target: 'B',
|
||||
targetHandle: 'default',
|
||||
data: { edgeType: 'default' },
|
||||
},
|
||||
{
|
||||
id: 'B-C',
|
||||
source: 'B',
|
||||
sourceHandle: 'default',
|
||||
target: 'C',
|
||||
targetHandle: 'default',
|
||||
data: { edgeType: 'default' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = transformFilterNodesAsEdges({
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
// Should only have nodes A and C
|
||||
expect(result.nodes).toEqual([
|
||||
{
|
||||
id: 'A',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: 'Step A',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'A',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Step C',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 300 },
|
||||
stepId: 'C',
|
||||
},
|
||||
position: { x: 0, y: 300 },
|
||||
},
|
||||
]);
|
||||
|
||||
// Should have one edge with filter data
|
||||
expect(result.edges).toHaveLength(1);
|
||||
expect(result.edges[0]).toEqual({
|
||||
id: 'A-C-filter-B',
|
||||
type: 'filter--editable',
|
||||
source: 'A',
|
||||
sourceHandle: 'default',
|
||||
target: 'C',
|
||||
targetHandle: 'default',
|
||||
selectable: true,
|
||||
deletable: true,
|
||||
data: {
|
||||
edgeType: 'filter',
|
||||
stepId: 'B',
|
||||
name: 'Filter B',
|
||||
runStatus: undefined,
|
||||
filterSettings: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple filter nodes', () => {
|
||||
const diagram: WorkflowDiagram = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: 'Step A',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'A',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: 'B1',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'FILTER',
|
||||
name: 'Filter B1',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 150 },
|
||||
stepId: 'B1',
|
||||
},
|
||||
position: { x: 0, y: 150 },
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Step C',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 300 },
|
||||
stepId: 'C',
|
||||
},
|
||||
position: { x: 0, y: 300 },
|
||||
},
|
||||
{
|
||||
id: 'B2',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'FILTER',
|
||||
name: 'Filter B2',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 450 },
|
||||
stepId: 'B2',
|
||||
},
|
||||
position: { x: 0, y: 450 },
|
||||
},
|
||||
{
|
||||
id: 'D',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CREATE_RECORD',
|
||||
name: 'Step D',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 600 },
|
||||
stepId: 'D',
|
||||
},
|
||||
position: { x: 0, y: 600 },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'A-B1',
|
||||
source: 'A',
|
||||
sourceHandle: 'default',
|
||||
target: 'B1',
|
||||
targetHandle: 'default',
|
||||
data: { edgeType: 'default' },
|
||||
},
|
||||
{
|
||||
id: 'B1-C',
|
||||
source: 'B1',
|
||||
sourceHandle: 'default',
|
||||
target: 'C',
|
||||
targetHandle: 'default',
|
||||
data: { edgeType: 'default' },
|
||||
},
|
||||
{
|
||||
id: 'C-B2',
|
||||
source: 'C',
|
||||
sourceHandle: 'default',
|
||||
target: 'B2',
|
||||
targetHandle: 'default',
|
||||
data: { edgeType: 'default' },
|
||||
},
|
||||
{
|
||||
id: 'B2-D',
|
||||
source: 'B2',
|
||||
sourceHandle: 'default',
|
||||
target: 'D',
|
||||
targetHandle: 'default',
|
||||
data: { edgeType: 'default' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = transformFilterNodesAsEdges({
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
// Should only have nodes A, C, and D
|
||||
expect(result.nodes).toHaveLength(3);
|
||||
expect(result.nodes.map((n) => n.id)).toEqual(
|
||||
expect.arrayContaining(['A', 'C', 'D']),
|
||||
);
|
||||
|
||||
// Should have two edges with filter data
|
||||
expect(result.edges).toHaveLength(2);
|
||||
|
||||
const edgeAC = result.edges.find(
|
||||
(e) => e.source === 'A' && e.target === 'C',
|
||||
);
|
||||
expect(edgeAC).toEqual({
|
||||
id: 'A-C-filter-B1',
|
||||
type: 'filter--editable',
|
||||
source: 'A',
|
||||
sourceHandle: 'default',
|
||||
target: 'C',
|
||||
targetHandle: 'default',
|
||||
selectable: true,
|
||||
deletable: true,
|
||||
data: {
|
||||
edgeType: 'filter',
|
||||
name: 'Filter B1',
|
||||
runStatus: undefined,
|
||||
stepId: 'B1',
|
||||
filterSettings: {},
|
||||
},
|
||||
});
|
||||
|
||||
const edgeCD = result.edges.find(
|
||||
(e) => e.source === 'C' && e.target === 'D',
|
||||
);
|
||||
expect(edgeCD).toEqual({
|
||||
id: 'C-D-filter-B2',
|
||||
type: 'filter--editable',
|
||||
source: 'C',
|
||||
sourceHandle: 'default',
|
||||
target: 'D',
|
||||
targetHandle: 'default',
|
||||
selectable: true,
|
||||
deletable: true,
|
||||
data: {
|
||||
edgeType: 'filter',
|
||||
name: 'Filter B2',
|
||||
runStatus: undefined,
|
||||
stepId: 'B2',
|
||||
filterSettings: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle filter nodes that are not part of a chain', () => {
|
||||
const diagram: WorkflowDiagram = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: 'Step A',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'A',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'FILTER',
|
||||
name: 'Filter B',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 150 },
|
||||
stepId: 'B',
|
||||
},
|
||||
position: { x: 0, y: 150 },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'A-B',
|
||||
source: 'A',
|
||||
sourceHandle: 'default',
|
||||
target: 'B',
|
||||
targetHandle: 'default',
|
||||
data: { edgeType: 'default' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = transformFilterNodesAsEdges({
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
// Should only have node A (filter node B is removed)
|
||||
expect(result.nodes).toEqual([
|
||||
{
|
||||
id: 'A',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: 'Step A',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'A',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
]);
|
||||
|
||||
// Should have no edges (original edge A-B is removed, no new edges created)
|
||||
expect(result.edges).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve trigger nodes', () => {
|
||||
const diagram: WorkflowDiagram = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'trigger',
|
||||
data: {
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
name: 'Trigger',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'trigger',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'FILTER',
|
||||
name: 'Filter B',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 150 },
|
||||
stepId: 'B',
|
||||
},
|
||||
position: { x: 0, y: 150 },
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Step C',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 300 },
|
||||
stepId: 'C',
|
||||
},
|
||||
position: { x: 0, y: 300 },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'trigger-B',
|
||||
source: 'trigger',
|
||||
sourceHandle: 'default',
|
||||
target: 'B',
|
||||
targetHandle: 'default',
|
||||
data: { edgeType: 'default' },
|
||||
},
|
||||
{
|
||||
id: 'B-C',
|
||||
source: 'B',
|
||||
sourceHandle: 'default',
|
||||
target: 'C',
|
||||
targetHandle: 'default',
|
||||
data: { edgeType: 'default' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = transformFilterNodesAsEdges({
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
// Should have trigger and C nodes
|
||||
expect(result.nodes).toEqual([
|
||||
{
|
||||
id: 'trigger',
|
||||
data: {
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
name: 'Trigger',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'trigger',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Step C',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 300 },
|
||||
stepId: 'C',
|
||||
},
|
||||
position: { x: 0, y: 300 },
|
||||
},
|
||||
]);
|
||||
|
||||
// Should have one edge with filter data
|
||||
expect(result.edges).toEqual([
|
||||
{
|
||||
id: 'trigger-C-filter-B',
|
||||
type: 'filter--editable',
|
||||
source: 'trigger',
|
||||
sourceHandle: 'default',
|
||||
target: 'C',
|
||||
targetHandle: 'default',
|
||||
selectable: true,
|
||||
deletable: true,
|
||||
data: {
|
||||
edgeType: 'filter',
|
||||
name: 'Filter B',
|
||||
runStatus: undefined,
|
||||
stepId: 'B',
|
||||
filterSettings: {},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
+3
-8
@@ -4,14 +4,12 @@ import {
|
||||
} from '@/workflow/types/Workflow';
|
||||
import {
|
||||
type WorkflowDiagramEdgeData,
|
||||
type WorkflowDiagramEdgeType,
|
||||
type WorkflowRunDiagram,
|
||||
type WorkflowRunDiagramNode,
|
||||
type WorkflowRunDiagramStepNodeData,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
|
||||
import { isStepNode } from '@/workflow/workflow-diagram/utils/isStepNode';
|
||||
import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
|
||||
@@ -87,11 +85,9 @@ export const generateWorkflowRunDiagram = ({
|
||||
|
||||
const stepInfo = stepInfos?.[parentNode.id];
|
||||
|
||||
const edgeType: WorkflowDiagramEdgeType = 'empty-filter--run';
|
||||
|
||||
return {
|
||||
...edge,
|
||||
type: edgeType,
|
||||
type: 'run',
|
||||
data: {
|
||||
...edge.data,
|
||||
edgeType: 'default',
|
||||
@@ -101,11 +97,10 @@ export const generateWorkflowRunDiagram = ({
|
||||
});
|
||||
|
||||
return {
|
||||
diagram: transformFilterNodesAsEdges({
|
||||
diagram: {
|
||||
nodes: workflowRunDiagramNodes,
|
||||
edges: workflowRunDiagramEdges,
|
||||
workflowContext: 'workflow-run',
|
||||
}),
|
||||
},
|
||||
stepToOpenByDefault,
|
||||
};
|
||||
};
|
||||
|
||||
+2
-2
@@ -8,11 +8,11 @@ export const getEdgeTypeBetweenTwoNodes = ({
|
||||
}): WorkflowDiagramEdgeType => {
|
||||
switch (workflowContext) {
|
||||
case 'workflow': {
|
||||
return 'empty-filter--editable';
|
||||
return 'editable';
|
||||
}
|
||||
case 'workflow-version':
|
||||
case 'workflow-run': {
|
||||
return 'empty-filter--readonly';
|
||||
return 'readonly';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
|
||||
import { type WorkflowDiagramEdgeType } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
|
||||
export const getFilterEdgeType = ({
|
||||
workflowContext,
|
||||
}: {
|
||||
workflowContext: WorkflowContext;
|
||||
}): WorkflowDiagramEdgeType => {
|
||||
switch (workflowContext) {
|
||||
case 'workflow': {
|
||||
return 'filter--editable';
|
||||
}
|
||||
case 'workflow-version': {
|
||||
return 'filter--readonly';
|
||||
}
|
||||
case 'workflow-run': {
|
||||
return 'filter--run';
|
||||
}
|
||||
}
|
||||
};
|
||||
+2
-2
@@ -41,8 +41,7 @@ export const getWorkflowDiagramColors = ({
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'FAILED':
|
||||
case 'STOPPED': {
|
||||
case 'FAILED': {
|
||||
return {
|
||||
selected: {
|
||||
background: theme.adaptiveColors.red1,
|
||||
@@ -60,6 +59,7 @@ export const getWorkflowDiagramColors = ({
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'STOPPED':
|
||||
case 'SUCCESS': {
|
||||
return {
|
||||
selected: {
|
||||
|
||||
+1
-8
@@ -2,7 +2,6 @@ import { type WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
|
||||
import { type WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
|
||||
import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const EMPTY_DIAGRAM: WorkflowDiagram = {
|
||||
@@ -21,15 +20,9 @@ export const getWorkflowVersionDiagram = ({
|
||||
return EMPTY_DIAGRAM;
|
||||
}
|
||||
|
||||
const diagram = generateWorkflowDiagram({
|
||||
return generateWorkflowDiagram({
|
||||
trigger: workflowVersion.trigger ?? undefined,
|
||||
steps: workflowVersion.steps ?? [],
|
||||
workflowContext,
|
||||
});
|
||||
|
||||
return transformFilterNodesAsEdges({
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
workflowContext,
|
||||
});
|
||||
};
|
||||
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
|
||||
import {
|
||||
type WorkflowDiagramEdge,
|
||||
type WorkflowDiagramNode,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getFilterEdgeType } from '@/workflow/workflow-diagram/utils/getFilterEdgeType';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const transformFilterNodesAsEdges = <
|
||||
T extends WorkflowDiagramNode,
|
||||
U extends WorkflowDiagramEdge,
|
||||
>({
|
||||
nodes,
|
||||
edges,
|
||||
workflowContext,
|
||||
}: {
|
||||
nodes: T[];
|
||||
edges: U[];
|
||||
workflowContext: WorkflowContext;
|
||||
}): { nodes: T[]; edges: U[] } => {
|
||||
const filterNodes = nodes.filter(
|
||||
(node) =>
|
||||
node.data.nodeType === 'action' &&
|
||||
'actionType' in node.data &&
|
||||
node.data.actionType === 'FILTER',
|
||||
);
|
||||
|
||||
if (filterNodes.length === 0) {
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
const resultNodes = nodes.filter(
|
||||
(node) => !filterNodes.some((filterNode) => filterNode.id === node.id),
|
||||
);
|
||||
|
||||
const resultEdges = [...edges];
|
||||
const edgesToRemove = new Set<string>();
|
||||
const edgesToAdd: typeof edges = [];
|
||||
|
||||
for (const filterNode of filterNodes) {
|
||||
const incomingEdge = edges.find((edge) => edge.target === filterNode.id);
|
||||
const outgoingEdge = edges.find((edge) => edge.source === filterNode.id);
|
||||
|
||||
if (isDefined(incomingEdge) && isDefined(outgoingEdge)) {
|
||||
if (
|
||||
filterNode.data.nodeType !== 'action' ||
|
||||
filterNode.data.actionType !== 'FILTER'
|
||||
) {
|
||||
throw new Error('Expected the filter node to be of action type');
|
||||
}
|
||||
|
||||
const filterEdgeType = getFilterEdgeType({ workflowContext });
|
||||
|
||||
const newEdge: U = {
|
||||
...incomingEdge,
|
||||
type: filterEdgeType,
|
||||
id: `${incomingEdge.source}-${outgoingEdge.target}-filter-${filterNode.id}`,
|
||||
target: outgoingEdge.target,
|
||||
selectable: true,
|
||||
deletable: true,
|
||||
data: {
|
||||
...incomingEdge.data,
|
||||
edgeType: 'filter',
|
||||
stepId: filterNode.id,
|
||||
// TODO: Get the filter settings from the filter node
|
||||
filterSettings: {},
|
||||
name: filterNode.data.name,
|
||||
runStatus: filterNode.data.runStatus,
|
||||
},
|
||||
};
|
||||
|
||||
edgesToAdd.push(newEdge);
|
||||
edgesToRemove.add(incomingEdge.id);
|
||||
edgesToRemove.add(outgoingEdge.id);
|
||||
} else {
|
||||
if (isDefined(incomingEdge)) {
|
||||
edgesToRemove.add(incomingEdge.id);
|
||||
}
|
||||
|
||||
if (isDefined(outgoingEdge)) {
|
||||
edgesToRemove.add(outgoingEdge.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const finalEdges = [
|
||||
...resultEdges.filter((edge) => !edgesToRemove.has(edge.id)),
|
||||
...edgesToAdd,
|
||||
];
|
||||
|
||||
return {
|
||||
nodes: resultNodes,
|
||||
edges: finalEdges,
|
||||
};
|
||||
};
|
||||
+2
-43
@@ -1,6 +1,3 @@
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu';
|
||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||
import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge';
|
||||
import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeButtonGroup';
|
||||
@@ -12,14 +9,12 @@ import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workf
|
||||
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
|
||||
import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps';
|
||||
import { getConnectionOptionsForSourceHandle } from '@/workflow/workflow-diagram/workflow-edges/utils/getConnectionOptionsForSourceHandle';
|
||||
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
|
||||
import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { EdgeLabelRenderer, getBezierPath } from '@xyflow/react';
|
||||
import { type MouseEvent, useContext } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { type MouseEvent } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconFilter, IconPlus, IconTrash } from 'twenty-ui/display';
|
||||
import { IconPlus, IconTrash } from 'twenty-ui/display';
|
||||
|
||||
type WorkflowDiagramDefaultEdgeEditableProps =
|
||||
WorkflowDiagramEdgeComponentProps;
|
||||
@@ -39,8 +34,6 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
|
||||
}: WorkflowDiagramDefaultEdgeEditableProps) => {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const { isInRightDrawer } = useContext(ActionMenuContext);
|
||||
|
||||
const { isEdgeHovered } = useEdgeState();
|
||||
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
@@ -50,8 +43,6 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
|
||||
targetY,
|
||||
});
|
||||
|
||||
const { createStep } = useCreateStep();
|
||||
|
||||
const { deleteEdge } = useDeleteEdge();
|
||||
|
||||
const { startNodeCreation, isNodeCreationStarted } = useStartNodeCreation();
|
||||
@@ -61,34 +52,6 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
|
||||
nextStepId: target,
|
||||
});
|
||||
|
||||
const setCommandMenuNavigationStack = useSetRecoilState(
|
||||
commandMenuNavigationStackState,
|
||||
);
|
||||
|
||||
const { openWorkflowEditFilterInCommandMenu } =
|
||||
useOpenWorkflowEditFilterInCommandMenu();
|
||||
|
||||
const handleCreateFilter = async () => {
|
||||
const createdStep = await createStep({
|
||||
newStepType: 'FILTER',
|
||||
parentStepId: source,
|
||||
nextStepId: target,
|
||||
});
|
||||
|
||||
if (!isDefined(createdStep)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isInRightDrawer) {
|
||||
setCommandMenuNavigationStack([]);
|
||||
}
|
||||
|
||||
openWorkflowEditFilterInCommandMenu({
|
||||
stepId: createdStep.id,
|
||||
stepName: createdStep.name,
|
||||
});
|
||||
};
|
||||
|
||||
const handleNodeButtonClick = () => {
|
||||
startNodeCreation({
|
||||
parentStepId: source,
|
||||
@@ -153,10 +116,6 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
|
||||
>
|
||||
<WorkflowDiagramEdgeButtonGroup
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconFilter,
|
||||
onClick: handleCreateFilter,
|
||||
},
|
||||
{
|
||||
Icon: IconPlus,
|
||||
onClick: handleNodeButtonClick,
|
||||
|
||||
-306
@@ -1,306 +0,0 @@
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu';
|
||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||
import { workflowDiagramPanOnDragComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramPanOnDragComponentState';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { type WorkflowDiagramEdgeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getWorkflowDiagramColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramColors';
|
||||
import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge';
|
||||
import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeButtonGroup';
|
||||
import { WorkflowDiagramEdgeLabel } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabel';
|
||||
import { WorkflowDiagramEdgeLabelContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabelContainer';
|
||||
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
|
||||
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
|
||||
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
|
||||
import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps';
|
||||
import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge';
|
||||
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
|
||||
import { WorkflowStepFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter';
|
||||
import { useFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterCounter';
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { EdgeLabelRenderer, getBezierPath } from '@xyflow/react';
|
||||
import { useContext } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
IconDotsVertical,
|
||||
IconFilter,
|
||||
IconFilterX,
|
||||
IconPlus,
|
||||
IconTrash,
|
||||
} from 'twenty-ui/display';
|
||||
import { IconButtonGroup } from 'twenty-ui/input';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
type WorkflowDiagramFilterEdgeEditableProps = WorkflowDiagramEdgeComponentProps;
|
||||
|
||||
const assertFilterEdgeDataOrThrow: (
|
||||
data: WorkflowDiagramEdgeData | undefined,
|
||||
) => asserts data is WorkflowDiagramEdgeData & { edgeType: 'filter' } = (
|
||||
data: WorkflowDiagramEdgeData | undefined,
|
||||
) => {
|
||||
if (data?.edgeType !== 'filter') {
|
||||
throw new Error('Edge data must be of type "filter"');
|
||||
}
|
||||
};
|
||||
|
||||
const StyledConfiguredFilterContainer = styled.div`
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const StyledIconButtonGroup = styled(IconButtonGroup)<{ selected?: boolean }>`
|
||||
pointer-events: all;
|
||||
|
||||
${({ selected, theme }) => {
|
||||
if (!selected) return '';
|
||||
const colors = getWorkflowDiagramColors({ theme });
|
||||
return css`
|
||||
background-color: ${colors.selected.background};
|
||||
border: 1px solid ${colors.selected.borderColor};
|
||||
`;
|
||||
}}
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramFilterEdgeEditable = ({
|
||||
source,
|
||||
sourceHandleId,
|
||||
target,
|
||||
targetHandleId,
|
||||
sourceY,
|
||||
sourceX,
|
||||
targetY,
|
||||
targetX,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
data,
|
||||
}: WorkflowDiagramFilterEdgeEditableProps) => {
|
||||
assertFilterEdgeDataOrThrow(data);
|
||||
|
||||
const { t, i18n } = useLingui();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
const { isInRightDrawer } = useContext(ActionMenuContext);
|
||||
|
||||
const { deleteStep } = useDeleteStep();
|
||||
const { deleteEdge } = useDeleteEdge();
|
||||
const { startNodeCreation, isNodeCreationStarted } = useStartNodeCreation();
|
||||
|
||||
const setCommandMenuNavigationStack = useSetRecoilState(
|
||||
commandMenuNavigationStackState,
|
||||
);
|
||||
|
||||
const { openDropdown } = useOpenDropdown();
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const { isEdgeHovered } = useEdgeState();
|
||||
|
||||
const setWorkflowDiagramPanOnDrag = useSetRecoilComponentState(
|
||||
workflowDiagramPanOnDragComponentState,
|
||||
);
|
||||
|
||||
const nodeCreationStarted = isNodeCreationStarted({
|
||||
parentStepId: data.stepId,
|
||||
nextStepId: target,
|
||||
});
|
||||
|
||||
const workflowSelectedNode = useRecoilComponentValue(
|
||||
workflowSelectedNodeComponentState,
|
||||
);
|
||||
|
||||
const isFilterNodeSelected =
|
||||
isNonEmptyString(data.stepId) && workflowSelectedNode === data.stepId;
|
||||
|
||||
const dropdownId = `${WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}-${source}-${target}`;
|
||||
|
||||
const isDropdownOpen = useRecoilComponentValue(
|
||||
isDropdownOpenComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const { openWorkflowEditFilterInCommandMenu } =
|
||||
useOpenWorkflowEditFilterInCommandMenu();
|
||||
|
||||
const handleFilterButtonClick = () => {
|
||||
if (!isInRightDrawer) {
|
||||
setCommandMenuNavigationStack([]);
|
||||
}
|
||||
|
||||
openWorkflowEditFilterInCommandMenu({
|
||||
stepId: data.stepId,
|
||||
stepName: data.name,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveFilterButtonClick = async () => {
|
||||
closeDropdown(dropdownId);
|
||||
|
||||
await deleteStep(data.stepId);
|
||||
};
|
||||
|
||||
const handleAddNodeButtonClick = () => {
|
||||
closeDropdown(dropdownId);
|
||||
|
||||
startNodeCreation({
|
||||
parentStepId: data.stepId,
|
||||
nextStepId: target,
|
||||
position: { x: labelX, y: labelY },
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteBranchClick = async () => {
|
||||
closeDropdown(dropdownId);
|
||||
|
||||
await deleteEdge({
|
||||
source,
|
||||
target,
|
||||
});
|
||||
};
|
||||
|
||||
const { filterCounter } = useFilterCounter({ stepId: data.stepId });
|
||||
const { unselected } = getWorkflowDiagramColors({ theme });
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowDiagramBaseEdge
|
||||
source={source}
|
||||
sourceHandleId={sourceHandleId}
|
||||
target={target}
|
||||
targetHandleId={targetHandleId}
|
||||
path={edgePath}
|
||||
markerStart={markerStart}
|
||||
markerEnd={markerEnd}
|
||||
/>
|
||||
|
||||
<EdgeLabelRenderer>
|
||||
{isDefined(data?.labelOptions) && (
|
||||
<WorkflowDiagramEdgeLabelContainer
|
||||
sourceX={sourceX}
|
||||
sourceY={sourceY}
|
||||
position={data.labelOptions.position}
|
||||
>
|
||||
<WorkflowDiagramEdgeLabel label={i18n._(data.labelOptions.label)} />
|
||||
</WorkflowDiagramEdgeLabelContainer>
|
||||
)}
|
||||
|
||||
<WorkflowDiagramEdgeV2Container
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
||||
labelX={labelX}
|
||||
labelY={labelY}
|
||||
>
|
||||
<WorkflowDiagramEdgeV2VisibilityContainer shouldDisplay>
|
||||
<StyledConfiguredFilterContainer>
|
||||
<WorkflowStepFilterCounter
|
||||
backgroundColor={unselected.tagBackground}
|
||||
textColor={theme.font.color.inverted}
|
||||
counter={filterCounter}
|
||||
/>
|
||||
{isEdgeHovered({
|
||||
source,
|
||||
target,
|
||||
sourceHandle: sourceHandleId,
|
||||
targetHandle: targetHandleId,
|
||||
}) ||
|
||||
isDropdownOpen ||
|
||||
nodeCreationStarted ? (
|
||||
<WorkflowDiagramEdgeButtonGroup
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconFilter,
|
||||
onClick: handleFilterButtonClick,
|
||||
},
|
||||
{
|
||||
Icon: IconDotsVertical,
|
||||
onClick: () => {
|
||||
openDropdown({
|
||||
dropdownComponentInstanceIdFromProps: dropdownId,
|
||||
});
|
||||
},
|
||||
},
|
||||
]}
|
||||
selected={isFilterNodeSelected}
|
||||
/>
|
||||
) : (
|
||||
<StyledIconButtonGroup
|
||||
className="nodrag nopan"
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconFilter,
|
||||
onClick: handleFilterButtonClick,
|
||||
},
|
||||
]}
|
||||
selected={isFilterNodeSelected}
|
||||
/>
|
||||
)}
|
||||
</StyledConfiguredFilterContainer>
|
||||
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
clickableComponent={<div></div>}
|
||||
data-select-disable
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownStrategy="absolute"
|
||||
dropdownOffset={{
|
||||
x: 24,
|
||||
y: 4,
|
||||
}}
|
||||
onOpen={() => {
|
||||
setWorkflowDiagramPanOnDrag(false);
|
||||
}}
|
||||
onClose={() => {
|
||||
setWorkflowDiagramPanOnDrag(true);
|
||||
}}
|
||||
dropdownComponents={
|
||||
<DropdownContent
|
||||
widthInPixels={GenericDropdownContentWidth.Narrow}
|
||||
>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
text={t`Remove filter`}
|
||||
LeftIcon={IconFilterX}
|
||||
onClick={handleRemoveFilterButtonClick}
|
||||
/>
|
||||
<MenuItem
|
||||
text={t`Add Node`}
|
||||
LeftIcon={IconPlus}
|
||||
onClick={handleAddNodeButtonClick}
|
||||
/>
|
||||
<MenuItem
|
||||
text={t`Delete branch`}
|
||||
LeftIcon={IconTrash}
|
||||
onClick={handleDeleteBranchClick}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
</WorkflowDiagramEdgeV2VisibilityContainer>
|
||||
</WorkflowDiagramEdgeV2Container>
|
||||
</EdgeLabelRenderer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-143
@@ -1,143 +0,0 @@
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useOpenWorkflowViewFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { type WorkflowDiagramEdgeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getWorkflowDiagramColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramColors';
|
||||
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
|
||||
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
|
||||
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||
import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps';
|
||||
import { WorkflowStepFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter';
|
||||
import { useFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterCounter';
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from '@xyflow/react';
|
||||
import { useContext } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { IconFilter } from 'twenty-ui/display';
|
||||
import { IconButtonGroup } from 'twenty-ui/input';
|
||||
|
||||
type WorkflowDiagramFilterEdgeReadonlyProps = WorkflowDiagramEdgeComponentProps;
|
||||
|
||||
const assertFilterEdgeDataOrThrow: (
|
||||
data: WorkflowDiagramEdgeData | undefined,
|
||||
) => asserts data is WorkflowDiagramEdgeData & { edgeType: 'filter' } = (
|
||||
data: WorkflowDiagramEdgeData | undefined,
|
||||
) => {
|
||||
if (data?.edgeType !== 'filter') {
|
||||
throw new Error('Edge data must be of type "filter"');
|
||||
}
|
||||
};
|
||||
|
||||
const StyledIconButtonGroup = styled(IconButtonGroup)<{ selected?: boolean }>`
|
||||
pointer-events: all;
|
||||
|
||||
${({ selected, theme }) => {
|
||||
if (!selected) return '';
|
||||
const colors = getWorkflowDiagramColors({ theme });
|
||||
return css`
|
||||
background-color: ${colors.selected.background};
|
||||
border: 1px solid ${colors.selected.borderColor};
|
||||
`;
|
||||
}}
|
||||
`;
|
||||
|
||||
const StyledConfiguredFilterContainer = styled.div`
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramFilterEdgeReadonly = ({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
data,
|
||||
}: WorkflowDiagramFilterEdgeReadonlyProps) => {
|
||||
assertFilterEdgeDataOrThrow(data);
|
||||
|
||||
const { isInRightDrawer } = useContext(ActionMenuContext);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
const workflowSelectedNode = useRecoilComponentValue(
|
||||
workflowSelectedNodeComponentState,
|
||||
);
|
||||
|
||||
const isFilterNodeSelected =
|
||||
isNonEmptyString(data.stepId) && workflowSelectedNode === data.stepId;
|
||||
|
||||
const { openWorkflowViewFilterInCommandMenu } =
|
||||
useOpenWorkflowViewFilterInCommandMenu();
|
||||
|
||||
const setCommandMenuNavigationStack = useSetRecoilState(
|
||||
commandMenuNavigationStackState,
|
||||
);
|
||||
|
||||
const handleFilterButtonClick = () => {
|
||||
if (!isInRightDrawer) {
|
||||
setCommandMenuNavigationStack([]);
|
||||
}
|
||||
|
||||
openWorkflowViewFilterInCommandMenu({
|
||||
stepId: data.stepId,
|
||||
stepName: data.name,
|
||||
});
|
||||
};
|
||||
|
||||
const { filterCounter } = useFilterCounter({
|
||||
stepId: data.stepId,
|
||||
});
|
||||
const { unselected } = getWorkflowDiagramColors({ theme });
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge
|
||||
markerStart={markerStart}
|
||||
markerEnd={markerEnd}
|
||||
path={edgePath}
|
||||
style={{ stroke: theme.border.color.strong }}
|
||||
/>
|
||||
|
||||
<EdgeLabelRenderer>
|
||||
<WorkflowDiagramEdgeV2Container
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
||||
labelX={labelX}
|
||||
labelY={labelY}
|
||||
>
|
||||
<WorkflowDiagramEdgeV2VisibilityContainer shouldDisplay>
|
||||
<StyledConfiguredFilterContainer>
|
||||
<WorkflowStepFilterCounter
|
||||
backgroundColor={unselected.tagBackground}
|
||||
textColor={theme.font.color.inverted}
|
||||
counter={filterCounter}
|
||||
/>
|
||||
<StyledIconButtonGroup
|
||||
className="nodrag nopan"
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconFilter,
|
||||
onClick: handleFilterButtonClick,
|
||||
},
|
||||
]}
|
||||
selected={isFilterNodeSelected}
|
||||
/>
|
||||
</StyledConfiguredFilterContainer>
|
||||
</WorkflowDiagramEdgeV2VisibilityContainer>
|
||||
</WorkflowDiagramEdgeV2Container>
|
||||
</EdgeLabelRenderer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import type { WorkflowRunStepStatus } from '@/workflow/types/Workflow';
|
||||
import { useOpenWorkflowRunFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { type WorkflowDiagramEdgeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getWorkflowDiagramColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramColors';
|
||||
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
|
||||
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
|
||||
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowRunDiagramBaseEdge';
|
||||
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||
import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps';
|
||||
import { WorkflowStepFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter';
|
||||
import { useFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterCounter';
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { EdgeLabelRenderer, getBezierPath } from '@xyflow/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconFilter } from 'twenty-ui/display';
|
||||
import { IconButtonGroup } from 'twenty-ui/input';
|
||||
|
||||
type WorkflowDiagramFilterEdgeRunProps = WorkflowDiagramEdgeComponentProps;
|
||||
|
||||
const assertFilterEdgeDataOrThrow: (
|
||||
data: WorkflowDiagramEdgeData | undefined,
|
||||
) => asserts data is WorkflowDiagramEdgeData & { edgeType: 'filter' } = (
|
||||
data: WorkflowDiagramEdgeData | undefined,
|
||||
) => {
|
||||
if (data?.edgeType !== 'filter') {
|
||||
throw new Error('Edge data must be of type "filter"');
|
||||
}
|
||||
};
|
||||
|
||||
const StyledIconButtonGroup = styled(IconButtonGroup)<{
|
||||
selected?: boolean;
|
||||
runStatus?: WorkflowRunStepStatus;
|
||||
}>`
|
||||
pointer-events: all;
|
||||
|
||||
${({ selected, runStatus, theme }) => {
|
||||
if (!selected) return '';
|
||||
const colors = getWorkflowDiagramColors({ runStatus, theme });
|
||||
return css`
|
||||
background-color: ${colors.selected.background};
|
||||
border: 1px solid ${colors.selected.borderColor};
|
||||
`;
|
||||
}}
|
||||
`;
|
||||
|
||||
const StyledConfiguredFilterContainer = styled.div`
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramFilterEdgeRun = ({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
data,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
}: WorkflowDiagramFilterEdgeRunProps) => {
|
||||
assertFilterEdgeDataOrThrow(data);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
const workflowSelectedNode = useRecoilComponentValue(
|
||||
workflowSelectedNodeComponentState,
|
||||
);
|
||||
|
||||
const isFilterNodeSelected =
|
||||
isNonEmptyString(data.stepId) && workflowSelectedNode === data.stepId;
|
||||
|
||||
const { openWorkflowRunFilterInCommandMenu } =
|
||||
useOpenWorkflowRunFilterInCommandMenu();
|
||||
|
||||
const handleFilterButtonClick = () => {
|
||||
if (!isDefined(data.runStatus)) {
|
||||
throw new Error('Run status must be set on edge data for workflow runs');
|
||||
}
|
||||
|
||||
openWorkflowRunFilterInCommandMenu({
|
||||
stepId: data.stepId,
|
||||
stepName: data.name,
|
||||
stepExecutionStatus: data.runStatus,
|
||||
});
|
||||
};
|
||||
|
||||
const { filterCounter } = useFilterCounter({ stepId: data.stepId });
|
||||
const { selected } = getWorkflowDiagramColors({
|
||||
theme,
|
||||
runStatus: data.runStatus,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowRunDiagramBaseEdge
|
||||
path={edgePath}
|
||||
markerStart={markerStart}
|
||||
markerEnd={markerEnd}
|
||||
/>
|
||||
|
||||
<EdgeLabelRenderer>
|
||||
<WorkflowDiagramEdgeV2Container
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
||||
labelX={labelX}
|
||||
labelY={labelY}
|
||||
>
|
||||
<WorkflowDiagramEdgeV2VisibilityContainer shouldDisplay>
|
||||
<StyledConfiguredFilterContainer>
|
||||
<WorkflowStepFilterCounter
|
||||
backgroundColor={selected.tagBackground}
|
||||
textColor={selected.color}
|
||||
counter={filterCounter}
|
||||
/>
|
||||
<StyledIconButtonGroup
|
||||
className="nodrag nopan"
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconFilter,
|
||||
onClick: handleFilterButtonClick,
|
||||
},
|
||||
]}
|
||||
selected={isFilterNodeSelected}
|
||||
runStatus={data.runStatus}
|
||||
/>
|
||||
</StyledConfiguredFilterContainer>
|
||||
</WorkflowDiagramEdgeV2VisibilityContainer>
|
||||
</WorkflowDiagramEdgeV2Container>
|
||||
</EdgeLabelRenderer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
import { EDGE_BRANCH_ARROW_MARKER } from '@/workflow/workflow-diagram/workflow-edges/constants/EdgeBranchArrowMarker';
|
||||
import {
|
||||
type WorkflowDiagramEdge,
|
||||
type WorkflowDiagramEdgeType,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { EDGE_BRANCH_ARROW_MARKER } from '@/workflow/workflow-diagram/workflow-edges/constants/EdgeBranchArrowMarker';
|
||||
|
||||
export const WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION = {
|
||||
type: 'empty-filter--readonly' satisfies WorkflowDiagramEdgeType,
|
||||
type: 'readonly' satisfies WorkflowDiagramEdgeType,
|
||||
markerStart: undefined,
|
||||
...EDGE_BRANCH_ARROW_MARKER.Default,
|
||||
deletable: false,
|
||||
|
||||
+2
-1
@@ -122,7 +122,8 @@ export const WorkflowRunDiagramStepNode = ({
|
||||
{capitalize(data.nodeType)}
|
||||
</WorkflowNodeLabel>
|
||||
|
||||
{data.runStatus === StepStatus.SUCCESS && (
|
||||
{(data.runStatus === StepStatus.SUCCESS ||
|
||||
data.runStatus === StepStatus.STOPPED) && (
|
||||
<StyledNodeCounter>
|
||||
<StyledColorIcon color={theme.tag.background.turquoise}>
|
||||
<IconCheck color={theme.tag.text.turquoise} size={14} />
|
||||
|
||||
+7
-2
@@ -1,8 +1,8 @@
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
|
||||
export const OTHER_ACTIONS: Array<{
|
||||
export const FLOW_ACTIONS: Array<{
|
||||
label: string;
|
||||
type: Extract<WorkflowActionType, 'ITERATOR'>;
|
||||
type: Extract<WorkflowActionType, 'ITERATOR' | 'FILTER'>;
|
||||
icon: string;
|
||||
}> = [
|
||||
{
|
||||
@@ -10,4 +10,9 @@ export const OTHER_ACTIONS: Array<{
|
||||
type: 'ITERATOR',
|
||||
icon: 'IconRepeat',
|
||||
},
|
||||
{
|
||||
label: 'Filter',
|
||||
type: 'FILTER',
|
||||
icon: 'IconFilter',
|
||||
},
|
||||
];
|
||||
+11
-2
@@ -18,6 +18,7 @@ import { type StepOutputSchemaV2 } from '@/workflow/workflow-variables/types/Ste
|
||||
import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/extractRawVariableNamePart';
|
||||
import { getCurrentSubStepFromPath } from '@/workflow/workflow-variables/utils/getCurrentSubStepFromPath';
|
||||
import { getStepHeaderLabel } from '@/workflow/workflow-variables/utils/getStepHeaderLabel';
|
||||
import { getStepItemIcon } from '@/workflow/workflow-variables/utils/getStepItemIcon';
|
||||
import { getVariableTemplateFromPath } from '@/workflow/workflow-variables/utils/getVariableTemplateFromPath';
|
||||
import { searchVariableThroughOutputSchemaV2 } from '@/workflow/workflow-variables/utils/searchVariableThroughOutputSchemaV2';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
@@ -185,7 +186,7 @@ export const WorkflowDropdownStepOutputItems = ({
|
||||
subStepObjectMetadataItem.labelSingular
|
||||
.toLowerCase()
|
||||
.includes(searchInputValue.toLowerCase())
|
||||
: true;
|
||||
: isDefined(displayedSubStepObject);
|
||||
|
||||
const objectLabel = subStepObjectMetadataItem?.labelSingular;
|
||||
|
||||
@@ -236,7 +237,15 @@ export const WorkflowDropdownStepOutputItems = ({
|
||||
onClick={() => handleSelectField(key)}
|
||||
text={subStep.label || key}
|
||||
hasSubMenu={!subStep.isLeaf}
|
||||
LeftIcon={subStep.icon ? getIcon(subStep.icon) : undefined}
|
||||
LeftIcon={
|
||||
subStep.icon
|
||||
? getIcon(subStep.icon)
|
||||
: getIcon(
|
||||
getStepItemIcon({
|
||||
itemType: subStep.type,
|
||||
}),
|
||||
)
|
||||
}
|
||||
contextualText={
|
||||
subStep.isLeaf ? subStep?.value?.toString() : undefined
|
||||
}
|
||||
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledFilterCounter = styled.div<{
|
||||
backgroundColor: string;
|
||||
textColor: string;
|
||||
}>`
|
||||
align-items: center;
|
||||
background: ${({ backgroundColor }) => backgroundColor};
|
||||
border-radius: 50%;
|
||||
color: ${({ textColor }) => textColor};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.xxs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
height: 12px;
|
||||
justify-content: center;
|
||||
width: 12px;
|
||||
left: -3px;
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
type WorkflowStepFilterCounterProps = {
|
||||
backgroundColor: string;
|
||||
textColor: string;
|
||||
counter: number;
|
||||
};
|
||||
|
||||
export const WorkflowStepFilterCounter = ({
|
||||
backgroundColor,
|
||||
textColor,
|
||||
counter,
|
||||
}: WorkflowStepFilterCounterProps) => (
|
||||
<StyledFilterCounter backgroundColor={backgroundColor} textColor={textColor}>
|
||||
{counter}
|
||||
</StyledFilterCounter>
|
||||
);
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import { WorkflowStepFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter';
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { expect, within } from '@storybook/test';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { THEME_LIGHT } from 'twenty-ui/theme';
|
||||
|
||||
const meta: Meta<typeof WorkflowStepFilterCounter> = {
|
||||
title: 'Modules/Workflow/Actions/Filter/WorkflowStepFilterCounter',
|
||||
component: WorkflowStepFilterCounter,
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof WorkflowStepFilterCounter>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
counter: 1,
|
||||
backgroundColor: THEME_LIGHT.border.color.strong,
|
||||
textColor: THEME_LIGHT.font.color.inverted,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(await canvas.findByText('1')).toBeVisible();
|
||||
},
|
||||
};
|
||||
+1
-2
@@ -93,11 +93,10 @@ export const WorkflowVariablesDropdownStepItems = ({
|
||||
.includes(searchInputValue.toLowerCase())
|
||||
: true;
|
||||
|
||||
const objectLabel = displayedSubStepObjectMetadata?.labelSingular;
|
||||
const shouldDisplaySubStepObject =
|
||||
shouldDisplayRecordObjects && isObjectFoundThroughSearch;
|
||||
|
||||
const objectLabel = displayedSubStepObjectMetadata?.labelSingular;
|
||||
|
||||
return (
|
||||
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
|
||||
<DropdownMenuHeader
|
||||
|
||||
-190
@@ -746,195 +746,5 @@ describe('WorkflowVersionEdgeWorkspaceService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with filter steps', () => {
|
||||
it('should delete the filter step when deleting edge from trigger to target through filter', async () => {
|
||||
const mockStepsWithFilter = [
|
||||
{
|
||||
id: 'step-1',
|
||||
type: WorkflowActionType.FORM,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: ['step-2'],
|
||||
},
|
||||
{
|
||||
id: 'step-2',
|
||||
type: WorkflowActionType.SEND_EMAIL,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: [],
|
||||
},
|
||||
{
|
||||
id: 'filter-step',
|
||||
type: WorkflowActionType.FILTER,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: ['step-2'],
|
||||
},
|
||||
] as WorkflowAction[];
|
||||
|
||||
const mockTriggerWithFilter = {
|
||||
type: WorkflowTriggerType.MANUAL,
|
||||
settings: {},
|
||||
nextStepIds: ['step-1', 'filter-step'],
|
||||
};
|
||||
|
||||
const mockWorkflowVersionWithFilter = {
|
||||
id: mockWorkflowVersionId,
|
||||
trigger: mockTriggerWithFilter,
|
||||
steps: mockStepsWithFilter,
|
||||
status: 'DRAFT',
|
||||
} as WorkflowVersionWorkspaceEntity;
|
||||
|
||||
workflowCommonWorkspaceService.getWorkflowVersionOrFail.mockResolvedValue(
|
||||
mockWorkflowVersionWithFilter,
|
||||
);
|
||||
|
||||
const result = await service.deleteWorkflowVersionEdge({
|
||||
source: TRIGGER_STEP_ID,
|
||||
target: 'step-2',
|
||||
workflowVersionId: mockWorkflowVersionId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
|
||||
expect(
|
||||
workflowCommonWorkspaceService.getWorkflowVersionOrFail,
|
||||
).toHaveBeenCalledWith({
|
||||
workflowVersionId: mockWorkflowVersionId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
|
||||
expect(
|
||||
mockWorkflowVersionWorkspaceRepository.update,
|
||||
).toHaveBeenCalledWith(mockWorkflowVersionId, {
|
||||
trigger: {
|
||||
...mockTriggerWithFilter,
|
||||
nextStepIds: ['step-1'],
|
||||
},
|
||||
steps: mockStepsWithFilter.filter(
|
||||
(step) => step.id !== 'filter-step',
|
||||
),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
triggerNextStepIds: ['step-1'],
|
||||
stepsNextStepIds: {
|
||||
'step-1': ['step-2'],
|
||||
'step-2': [],
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should delete the filter step when deleting edge from step to target through filter', async () => {
|
||||
const mockStepsWithFilter = [
|
||||
{
|
||||
id: 'step-1',
|
||||
type: WorkflowActionType.FORM,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: ['step-2', 'filter-step'],
|
||||
},
|
||||
{
|
||||
id: 'step-2',
|
||||
type: WorkflowActionType.SEND_EMAIL,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: [],
|
||||
},
|
||||
{
|
||||
id: 'step-3',
|
||||
type: WorkflowActionType.SEND_EMAIL,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: [],
|
||||
},
|
||||
{
|
||||
id: 'filter-step',
|
||||
type: WorkflowActionType.FILTER,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: ['step-3'],
|
||||
},
|
||||
] as WorkflowAction[];
|
||||
|
||||
const mockWorkflowVersionWithFilter = {
|
||||
id: mockWorkflowVersionId,
|
||||
trigger: mockTrigger,
|
||||
steps: mockStepsWithFilter,
|
||||
status: 'DRAFT',
|
||||
} as WorkflowVersionWorkspaceEntity;
|
||||
|
||||
workflowCommonWorkspaceService.getWorkflowVersionOrFail.mockResolvedValue(
|
||||
mockWorkflowVersionWithFilter,
|
||||
);
|
||||
|
||||
const result = await service.deleteWorkflowVersionEdge({
|
||||
source: 'step-1',
|
||||
target: 'step-3',
|
||||
workflowVersionId: mockWorkflowVersionId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
|
||||
expect(
|
||||
workflowCommonWorkspaceService.getWorkflowVersionOrFail,
|
||||
).toHaveBeenCalledWith({
|
||||
workflowVersionId: mockWorkflowVersionId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
|
||||
expect(
|
||||
mockWorkflowVersionWorkspaceRepository.update,
|
||||
).toHaveBeenCalledWith(mockWorkflowVersionId, {
|
||||
steps: mockStepsWithFilter
|
||||
.map((step) => {
|
||||
if (step.id === 'step-1') {
|
||||
return {
|
||||
...step,
|
||||
nextStepIds: ['step-2'],
|
||||
};
|
||||
}
|
||||
|
||||
return step;
|
||||
})
|
||||
.filter((step) => step.id !== 'filter-step'),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
triggerNextStepIds: ['step-1'],
|
||||
stepsNextStepIds: {
|
||||
'step-1': ['step-2'],
|
||||
'step-2': [],
|
||||
'step-3': [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+13
-138
@@ -222,6 +222,16 @@ export class WorkflowVersionEdgeWorkspaceService {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
sourceStep.nextStepIds?.includes(target) &&
|
||||
!isDefined(sourceConnectionOptions)
|
||||
) {
|
||||
return computeWorkflowVersionStepChanges({
|
||||
trigger,
|
||||
steps,
|
||||
});
|
||||
}
|
||||
|
||||
const { updatedSourceStep, shouldPersist } = isDefined(
|
||||
sourceConnectionOptions,
|
||||
)
|
||||
@@ -364,12 +374,9 @@ export class WorkflowVersionEdgeWorkspaceService {
|
||||
}
|
||||
|
||||
if (!trigger.nextStepIds?.includes(target)) {
|
||||
return this.handleFilterBetweenTriggerAndTarget({
|
||||
return computeWorkflowVersionStepChanges({
|
||||
trigger,
|
||||
steps,
|
||||
target,
|
||||
workflowVersionId: workflowVersion.id,
|
||||
workflowVersionRepository,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -416,24 +423,13 @@ export class WorkflowVersionEdgeWorkspaceService {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Remove this once we start using filters as regular steps
|
||||
const isIteratorWithLoopTarget =
|
||||
isDefined(sourceConnectionOptions) &&
|
||||
sourceConnectionOptions.connectedStepType ===
|
||||
WorkflowActionType.ITERATOR &&
|
||||
sourceConnectionOptions.settings.isConnectedToLoop;
|
||||
|
||||
if (
|
||||
!sourceStep.nextStepIds?.includes(target) &&
|
||||
!isIteratorWithLoopTarget
|
||||
!isDefined(sourceConnectionOptions)
|
||||
) {
|
||||
return await this.handleFilterBetweenSourceAndTarget({
|
||||
return computeWorkflowVersionStepChanges({
|
||||
trigger,
|
||||
steps,
|
||||
sourceStep,
|
||||
target,
|
||||
workflowVersionId: workflowVersion.id,
|
||||
workflowVersionRepository,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -480,109 +476,6 @@ export class WorkflowVersionEdgeWorkspaceService {
|
||||
});
|
||||
}
|
||||
|
||||
private async handleFilterBetweenTriggerAndTarget({
|
||||
trigger,
|
||||
steps,
|
||||
target,
|
||||
workflowVersionId,
|
||||
workflowVersionRepository,
|
||||
}: {
|
||||
trigger: WorkflowTrigger;
|
||||
steps: WorkflowAction[];
|
||||
target: string;
|
||||
workflowVersionId: string;
|
||||
workflowVersionRepository: WorkspaceRepository<WorkflowVersionWorkspaceEntity>;
|
||||
}): Promise<WorkflowVersionStepChangesDTO> {
|
||||
const filterBetweenTriggerAndTarget = this.findFilterBetweenNodes({
|
||||
steps,
|
||||
sourceNextStepIds: trigger.nextStepIds,
|
||||
target,
|
||||
});
|
||||
|
||||
if (!isDefined(filterBetweenTriggerAndTarget)) {
|
||||
return computeWorkflowVersionStepChanges({
|
||||
trigger,
|
||||
steps,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedTrigger = {
|
||||
...trigger,
|
||||
nextStepIds: trigger.nextStepIds?.filter(
|
||||
(nextStepId: string) => nextStepId !== filterBetweenTriggerAndTarget.id,
|
||||
),
|
||||
};
|
||||
|
||||
const updatedSteps = steps.filter(
|
||||
(step) => step.id !== filterBetweenTriggerAndTarget.id,
|
||||
);
|
||||
|
||||
await workflowVersionRepository.update(workflowVersionId, {
|
||||
trigger: updatedTrigger,
|
||||
steps: updatedSteps,
|
||||
});
|
||||
|
||||
return computeWorkflowVersionStepChanges({
|
||||
trigger: updatedTrigger,
|
||||
steps: updatedSteps,
|
||||
});
|
||||
}
|
||||
|
||||
private async handleFilterBetweenSourceAndTarget({
|
||||
trigger,
|
||||
steps,
|
||||
sourceStep,
|
||||
target,
|
||||
workflowVersionRepository,
|
||||
workflowVersionId,
|
||||
}: {
|
||||
trigger: WorkflowTrigger | null;
|
||||
steps: WorkflowAction[];
|
||||
sourceStep: WorkflowAction;
|
||||
target: string;
|
||||
workflowVersionRepository: WorkspaceRepository<WorkflowVersionWorkspaceEntity>;
|
||||
workflowVersionId: string;
|
||||
}): Promise<WorkflowVersionStepChangesDTO> {
|
||||
const filterBetweenSourceAndTarget = this.findFilterBetweenNodes({
|
||||
steps,
|
||||
sourceNextStepIds: sourceStep.nextStepIds,
|
||||
target,
|
||||
});
|
||||
|
||||
if (!isDefined(filterBetweenSourceAndTarget)) {
|
||||
return computeWorkflowVersionStepChanges({
|
||||
trigger,
|
||||
steps,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedSourceStep = {
|
||||
...sourceStep,
|
||||
nextStepIds: sourceStep.nextStepIds?.filter(
|
||||
(nextStepId: string) => nextStepId !== filterBetweenSourceAndTarget.id,
|
||||
),
|
||||
};
|
||||
|
||||
const updatedSteps = steps
|
||||
.map((step) => {
|
||||
if (step.id === sourceStep.id) {
|
||||
return updatedSourceStep;
|
||||
}
|
||||
|
||||
return step;
|
||||
})
|
||||
.filter((step) => step.id !== filterBetweenSourceAndTarget.id);
|
||||
|
||||
await workflowVersionRepository.update(workflowVersionId, {
|
||||
steps: updatedSteps,
|
||||
});
|
||||
|
||||
return computeWorkflowVersionStepChanges({
|
||||
trigger,
|
||||
steps: updatedSteps,
|
||||
});
|
||||
}
|
||||
|
||||
private buildUpdatedSourceStepWithOptions({
|
||||
sourceStep,
|
||||
target,
|
||||
@@ -650,22 +543,4 @@ export class WorkflowVersionEdgeWorkspaceService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private findFilterBetweenNodes({
|
||||
steps,
|
||||
sourceNextStepIds,
|
||||
target,
|
||||
}: {
|
||||
steps: WorkflowAction[];
|
||||
sourceNextStepIds: string[] | undefined;
|
||||
target: string;
|
||||
}) {
|
||||
const nextStepFilters = steps.filter(
|
||||
(step) =>
|
||||
sourceNextStepIds?.includes(step.id) &&
|
||||
step.type === WorkflowActionType.FILTER,
|
||||
);
|
||||
|
||||
return nextStepFilters.find((step) => step.nextStepIds?.includes(target));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-95
@@ -151,100 +151,6 @@ describe('removeStep', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove step child that is a filter', () => {
|
||||
const step1 = createMockAction('1', ['2']);
|
||||
const step2 = createMockAction('2', ['3']);
|
||||
const step3 = {
|
||||
id: '3',
|
||||
name: 'Step 3',
|
||||
type: WorkflowActionType.FILTER,
|
||||
nextStepIds: ['4'],
|
||||
} as WorkflowAction;
|
||||
const step4 = createMockAction('4');
|
||||
|
||||
const result = removeStep({
|
||||
existingTrigger: mockTrigger,
|
||||
existingSteps: [step1, step2, step3, step4],
|
||||
stepIdToDelete: '2',
|
||||
stepToDeleteChildrenIds: ['3'],
|
||||
});
|
||||
|
||||
expect(result.updatedTrigger).toEqual(mockTrigger);
|
||||
expect(result.updatedSteps).toEqual([
|
||||
{ ...step1, nextStepIds: ['4'] },
|
||||
step4,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should remove trigger children that is a filter', () => {
|
||||
const step1 = {
|
||||
id: '1',
|
||||
name: 'Step 1',
|
||||
type: WorkflowActionType.FILTER,
|
||||
nextStepIds: ['2'],
|
||||
} as WorkflowAction;
|
||||
const step2 = createMockAction('2', ['3']);
|
||||
const step3 = createMockAction('3');
|
||||
|
||||
const result = removeStep({
|
||||
existingTrigger: mockTrigger,
|
||||
existingSteps: [step1, step2, step3],
|
||||
stepIdToDelete: TRIGGER_STEP_ID,
|
||||
stepToDeleteChildrenIds: ['1'],
|
||||
});
|
||||
|
||||
expect(result.updatedTrigger).toEqual(null);
|
||||
expect(result.updatedSteps).toEqual([step2, step3]);
|
||||
});
|
||||
|
||||
it('should remove filter step if it has no children', () => {
|
||||
const step1 = {
|
||||
id: '1',
|
||||
name: 'Step 1',
|
||||
type: WorkflowActionType.FILTER,
|
||||
nextStepIds: ['2'],
|
||||
} as WorkflowAction;
|
||||
const step2 = createMockAction('2', ['3']);
|
||||
const step3 = {
|
||||
id: '3',
|
||||
name: 'Step 3',
|
||||
type: WorkflowActionType.FILTER,
|
||||
nextStepIds: ['4'],
|
||||
} as WorkflowAction;
|
||||
const step4 = createMockAction('4');
|
||||
|
||||
const result = removeStep({
|
||||
existingTrigger: mockTrigger,
|
||||
existingSteps: [step1, step2, step3, step4],
|
||||
stepIdToDelete: '4',
|
||||
});
|
||||
|
||||
expect(result.updatedTrigger).toEqual(mockTrigger);
|
||||
expect(result.updatedSteps).toEqual([step1, { ...step2, nextStepIds: [] }]);
|
||||
});
|
||||
|
||||
it('should remove filter step if it is the last step', () => {
|
||||
const step1 = {
|
||||
id: '1',
|
||||
name: 'Step 1',
|
||||
type: WorkflowActionType.FILTER,
|
||||
nextStepIds: ['2'],
|
||||
} as WorkflowAction;
|
||||
const step2 = createMockAction('2');
|
||||
|
||||
const result = removeStep({
|
||||
existingTrigger: mockTrigger,
|
||||
existingSteps: [step1, step2],
|
||||
stepIdToDelete: '2',
|
||||
});
|
||||
|
||||
expect(result.updatedTrigger).toEqual({
|
||||
...mockTrigger,
|
||||
nextStepIds: [],
|
||||
});
|
||||
expect(result.updatedSteps).toEqual([]);
|
||||
});
|
||||
|
||||
it('should remove trigger if steps are null', () => {
|
||||
const result = removeStep({
|
||||
existingTrigger: { ...mockTrigger, nextStepIds: [] },
|
||||
@@ -253,7 +159,7 @@ describe('removeStep', () => {
|
||||
});
|
||||
|
||||
expect(result.updatedTrigger).toEqual(null);
|
||||
expect(result.updatedSteps).toEqual([]);
|
||||
expect(result.updatedSteps).toEqual(null);
|
||||
});
|
||||
|
||||
it('should handle removing a step that is part of iteratorLoopStepIds', () => {
|
||||
|
||||
+10
-134
@@ -29,7 +29,7 @@ const computeUpdatedNextStepIds = ({
|
||||
];
|
||||
};
|
||||
|
||||
const removeOneStep = ({
|
||||
export const removeStep = ({
|
||||
existingTrigger,
|
||||
existingSteps,
|
||||
stepIdToDelete,
|
||||
@@ -39,11 +39,15 @@ const removeOneStep = ({
|
||||
existingSteps: WorkflowAction[] | null;
|
||||
stepIdToDelete: string;
|
||||
stepToDeleteChildrenIds?: string[];
|
||||
}): {
|
||||
updatedSteps: WorkflowAction[];
|
||||
updatedTrigger: WorkflowTrigger | null;
|
||||
removedStepIds: string[];
|
||||
} => {
|
||||
}) => {
|
||||
if (stepIdToDelete === TRIGGER_STEP_ID) {
|
||||
return {
|
||||
updatedSteps: existingSteps,
|
||||
updatedTrigger: null,
|
||||
removedStepIds: [TRIGGER_STEP_ID],
|
||||
};
|
||||
}
|
||||
|
||||
const updatedSteps =
|
||||
existingSteps
|
||||
?.filter((step) => step.id !== stepIdToDelete)
|
||||
@@ -104,131 +108,3 @@ const removeOneStep = ({
|
||||
removedStepIds: [stepIdToDelete],
|
||||
};
|
||||
};
|
||||
|
||||
const removeRegularStep = ({
|
||||
existingTrigger,
|
||||
existingSteps,
|
||||
stepIdToDelete,
|
||||
stepToDeleteChildrenIds,
|
||||
}: {
|
||||
existingTrigger: WorkflowTrigger | null;
|
||||
existingSteps: WorkflowAction[] | null;
|
||||
stepIdToDelete: string;
|
||||
stepToDeleteChildrenIds?: string[];
|
||||
}): {
|
||||
updatedSteps: WorkflowAction[];
|
||||
updatedTrigger: WorkflowTrigger | null;
|
||||
removedStepIds: string[];
|
||||
} => {
|
||||
let { updatedSteps, updatedTrigger, removedStepIds } = removeOneStep({
|
||||
existingTrigger,
|
||||
existingSteps,
|
||||
stepIdToDelete,
|
||||
stepToDeleteChildrenIds,
|
||||
});
|
||||
|
||||
for (const stepId of stepToDeleteChildrenIds ?? []) {
|
||||
const step = existingSteps?.find((step) => step.id === stepId);
|
||||
|
||||
if (step?.type === WorkflowActionType.FILTER) {
|
||||
const {
|
||||
updatedSteps: stepsAfterRemovingChildFilter,
|
||||
updatedTrigger: triggerAfterRemovingChildFilter,
|
||||
removedStepIds: removedStepIdsAfterRemovingChildFilter,
|
||||
} = removeOneStep({
|
||||
existingTrigger: updatedTrigger,
|
||||
existingSteps: updatedSteps,
|
||||
stepIdToDelete: stepId,
|
||||
stepToDeleteChildrenIds: step.nextStepIds,
|
||||
});
|
||||
|
||||
updatedSteps = stepsAfterRemovingChildFilter;
|
||||
updatedTrigger = triggerAfterRemovingChildFilter;
|
||||
removedStepIds = [
|
||||
...removedStepIds,
|
||||
...removedStepIdsAfterRemovingChildFilter,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
for (const step of updatedSteps) {
|
||||
if (
|
||||
step?.type === WorkflowActionType.FILTER &&
|
||||
(!isDefined(step?.nextStepIds) || step.nextStepIds?.length === 0)
|
||||
) {
|
||||
const {
|
||||
updatedSteps: stepsAfterRemovingFilterWithoutChildren,
|
||||
updatedTrigger: triggerAfterRemovingFilterWithoutChildren,
|
||||
removedStepIds: removedStepIdsAfterRemovingFilterWithoutChildren,
|
||||
} = removeOneStep({
|
||||
existingTrigger: updatedTrigger,
|
||||
existingSteps: updatedSteps,
|
||||
stepIdToDelete: step.id,
|
||||
stepToDeleteChildrenIds: step.nextStepIds,
|
||||
});
|
||||
|
||||
updatedSteps = stepsAfterRemovingFilterWithoutChildren;
|
||||
updatedTrigger = triggerAfterRemovingFilterWithoutChildren;
|
||||
removedStepIds = [
|
||||
...removedStepIds,
|
||||
...removedStepIdsAfterRemovingFilterWithoutChildren,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
updatedSteps,
|
||||
updatedTrigger,
|
||||
removedStepIds,
|
||||
};
|
||||
};
|
||||
|
||||
const removeTrigger = ({
|
||||
existingSteps,
|
||||
triggerChildrenIds,
|
||||
}: {
|
||||
existingSteps: WorkflowAction[] | null;
|
||||
triggerChildrenIds?: string[];
|
||||
}) => {
|
||||
const stepIdsToRemove =
|
||||
triggerChildrenIds?.filter((id) => {
|
||||
const step = existingSteps?.find((step) => step.id === id);
|
||||
|
||||
return step?.type === WorkflowActionType.FILTER;
|
||||
}) ?? [];
|
||||
|
||||
const updatedSteps =
|
||||
existingSteps?.filter((step) => !stepIdsToRemove.includes(step.id)) ?? [];
|
||||
|
||||
return {
|
||||
updatedSteps,
|
||||
updatedTrigger: null,
|
||||
removedStepIds: [TRIGGER_STEP_ID, ...stepIdsToRemove],
|
||||
};
|
||||
};
|
||||
|
||||
export const removeStep = ({
|
||||
existingTrigger,
|
||||
existingSteps,
|
||||
stepIdToDelete,
|
||||
stepToDeleteChildrenIds,
|
||||
}: {
|
||||
existingTrigger: WorkflowTrigger | null;
|
||||
existingSteps: WorkflowAction[] | null;
|
||||
stepIdToDelete: string;
|
||||
stepToDeleteChildrenIds?: string[];
|
||||
}) => {
|
||||
if (stepIdToDelete === TRIGGER_STEP_ID) {
|
||||
return removeTrigger({
|
||||
existingSteps,
|
||||
triggerChildrenIds: stepToDeleteChildrenIds,
|
||||
});
|
||||
} else {
|
||||
return removeRegularStep({
|
||||
existingTrigger,
|
||||
existingSteps,
|
||||
stepIdToDelete,
|
||||
stepToDeleteChildrenIds,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user